20. 多重ループ

このレッスンでは、多重ループとその解消方法について学びます。

生きているキャラクターを選択する

現在のコードでは、攻撃対象がenemyParty[0]friendParty[0]に固定されています。そのため、0番目のキャラクターが倒れても攻撃し続けてしまい、戦闘が終わりません。

<!-- battle.html hidden -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>バトル</title>
    <link rel="stylesheet" href="css/battle.css">
    <script src="js/battle.js" defer></script>
</head>
<body>
    <table id="status">
        <tr>
            <th>ゆうしゃ</th><th>せんし</th><th>そうりょ</th><th>まほうつかい</th>
        </tr>
        <tr><td>HP 153</td><td>HP 198</td><td>HP 101</td><td>HP 77</td></tr>
        <tr><td>MP 25</td><td>MP 0</td><td>MP 35</td><td>MP 58</td></tr>
    </table>
    <div id="monster">
        <img class="dark-knight" src="img/dark-knight.png">
        <img class="dark-lord" src="img/dark-lord.png">
        <img class="demon-priest" src="img/demon-priest.png">
    </div>
    <div id="message">
        まおうがあらわれた。
    </div>
</body>
</html>
/* battle.css hidden */
body {
    background-color: rgb(34, 34, 34);
    color: white;
    font-family: sans-serif;
}
table#status {
    border: solid 2px white;
    border-collapse: collapse;
    margin: 10px auto 0 auto;
    width: 640px;
}
table#status tr:first-child {
    border-bottom: solid 1px white;
}
table#status td {
    text-align: center;
}
#monster {
    text-align: center;
    margin-top: 40px;
}
#monster .dark-lord {
    width: 400px;
}
#monster .dark-knight {
    width: 150px;
}
#monster .demon-priest {
    width: 150px;
}
#message {
    border: solid 2px white;
    border-radius: 4px;
    padding: 10px;
    width: 720px;
    margin: 30px auto;
}
// battle.js selection:66-87 highlight:72-73,77-79,82-84
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[0])) {
                displayMessage(`${enemyParty[0].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[0].defense);
            enemyParty[0].hp = calculateHp(enemyParty[0].hp, damage);
            displayDamageMessage(enemyParty[0].name, damage);
            await sleep();

            if (!isAlive(enemyParty[0])) {
                document.querySelector("#monster img:nth-child(1)").style.visibility = "hidden";
                displayDeadMessage(enemyParty[0].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[0])) {
                displayMessage(`${friendParty[0].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[0].defense);
            friendParty[0].hp = calculateHp(friendParty[0].hp, damage);
            displayDamageMessage(friendParty[0].name, damage);
            await sleep();
            document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${friendParty[0].hp}`;

            if (!isAlive(friendParty[0])) {
                displayDeadMessage(friendParty[0].name, true);
                await sleep();
            }
        }
    }
}

main();

生きているキャラクターを攻撃するように修正しましょう。まず、生きている敵キャラクターを探して、そのインデックスを変数target(ターゲット)に代入します。

// battle.js selection:71-77
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < enemyParty.length; i++) {
                if (isAlive(enemyParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[0])) {
                displayMessage(`${enemyParty[0].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[0].defense);
            enemyParty[0].hp = calculateHp(enemyParty[0].hp, damage);
            displayDamageMessage(enemyParty[0].name, damage);
            await sleep();

            if (!isAlive(enemyParty[0])) {
                document.querySelector("#monster img:nth-child(1)").style.visibility = "hidden";
                displayDeadMessage(enemyParty[0].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[0])) {
                displayMessage(`${friendParty[0].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[0].defense);
            friendParty[0].hp = calculateHp(friendParty[0].hp, damage);
            displayDamageMessage(friendParty[0].name, damage);
            await sleep();
            document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${friendParty[0].hp}`;

            if (!isAlive(friendParty[0])) {
                displayDeadMessage(friendParty[0].name, true);
                await sleep();
            }
        }
    }
}

main();

このtargetを使って、これまでenemyParty[0]としていた箇所をenemyParty[target]に書き換えれば、選択された敵キャラクターを攻撃するようになります。

// battle.js selection:79-95 highlight:81-82,86-88,91-93
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < enemyParty.length; i++) {
                if (isAlive(enemyParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[0])) {
                displayMessage(`${friendParty[0].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[0].defense);
            friendParty[0].hp = calculateHp(friendParty[0].hp, damage);
            displayDamageMessage(friendParty[0].name, damage);
            await sleep();
            document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${friendParty[0].hp}`;

            if (!isAlive(friendParty[0])) {
                displayDeadMessage(friendParty[0].name, true);
                await sleep();
            }
        }
    }
}

main();

敵の画像を非表示にする処理では、CSSセレクタのnth-childは1から始まるので、target + 1としています。

document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";

敵の攻撃コードを修正する

味方の攻撃コードと同様に、敵の攻撃コードも修正してみてください。friendPartyの中から生きているキャラクターを探し、攻撃対象にします。

// battle.js highlight:103-109,113-114,118-120,122,124-125 folded
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < enemyParty.length; i++) {
                if (isAlive(enemyParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < friendParty.length; i++) {
                if (isAlive(friendParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

多重ループの問題

コードは正しく動作するようになりましたが、構造を見てみると、ループの中にループがあり、さらにその中にループがある状態になっています。

while (true) {                              // 1段目: ターン全体のループ
    for (const character of friendParty) {  // 2段目: 味方全員の攻撃ループ
        // ...
        for (let i = 0; i < enemyParty.length; i++) {  // 3段目: 攻撃対象を探すループ
            // ...
        }
        // ...
    }
    // ...
}

このように、ループの中にループがある構造を多重ループと呼びます。

多重ループ自体は必ずしも悪いものではありません。しかし、ネストが深くなるとコードが読みにくくなり、何をしているのかを把握しづらくなります。

このような場合、内側のループを関数に切り出すことで、コードを読みやすくすることができます。

selectTarget関数

攻撃対象を探す処理をselectTarget関数として切り出しましょう。

// battle.js selection:45-52
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < enemyParty.length; i++) {
                if (isAlive(enemyParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }

            let target = 0;
            for (let i = 0; i < friendParty.length; i++) {
                if (isAlive(friendParty[i])) {
                    target = i;
                    break;
                }
            }

            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

この関数は、引数で受け取ったpartyの中から生きているキャラクターを探し、見つかったらそのインデックスを返します。見つからなかった場合は0を返します。

コードに適用する

selectTarget関数を使ってコードを書き換えてみましょう。

// battle.js selection:74-122 highlight:79,103
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

3重ループが2重ループに戻り、コードが読みやすくなりました。

HP表現の更新

レッスン13で関数について学んだとき、次のように説明しました。

HPを画面に反映する処理も同じように関数にしたいところですが、HPの表示蘭は「ゆうしゃ」の他に「せんし」「そうりょ」「まほうつかい」もあり、誰のHPを更新するかを指定する必要があります。他のパーティメンバーのHPも扱うようになったら、誰のHPを画面に反映するかを指定できる関数を作ることにしましょう。

今こそ、この関数を作るときです。パーティメンバーのインデックスを使って、画面上のHP表現を更新するdisplayHp関数を作りましょう。

味方のHP更新

現在のコードでは、味方のキャラクターが攻撃を受けた後にHP表示を更新しています。

// battle.js selection:115-115
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

これを関数にします。

// battle.js selection:45-48
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index) {
    const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
    document.querySelector(selector).textContent = `HP ${party[index].hp}`;
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

指定されたインデックスのキャラクターのHPを画面に反映します。

敵のHP更新

敵の場合は、HPの数値を表示する代わりに、倒れたら画像を非表示にしています。

// battle.js selection:98-98
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index) {
    const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
    document.querySelector(selector).textContent = `HP ${party[index].hp}`;
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

これも一種のHPの表現です。同じ関数に統合しましょう。

引数にisFriendを追加し、trueのとき(味方の場合)はHPを更新、falseのとき(敵の場合)は倒されたら画像を非表示にします。

// battle.js selection:45-57
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index, isFriend) {
    if (isFriend) {
        const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
        document.querySelector(selector).textContent = `HP ${party[index].hp}`;
    } else {
        const selector = `#monster img:nth-child(${index + 1})`;
        if (isAlive(party[index])) {
            document.querySelector(selector).style.visibility = "visible";
        } else {
            document.querySelector(selector).style.visibility = "hidden";
        }
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();

            if (!isAlive(enemyParty[target])) {
                document.querySelector(`#monster img:nth-child(${target + 1})`).style.visibility = "hidden";
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            document.querySelector(`#status tr:nth-child(2) td:nth-child(${target + 1})`).textContent = `HP ${friendParty[target].hp}`;

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

敵の場合は、isAliveで生存判定をして、生きている場合は画像を表示し、死んでいる場合は非表示にします。

コードに適用する

displayHp関数を使ってコードを書き換えてみましょう。

// battle.js selection:88-136 highlight:105,129
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index, isFriend) {
    if (isFriend) {
        const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
        document.querySelector(selector).textContent = `HP ${party[index].hp}`;
    } else {
        const selector = `#monster img:nth-child(${index + 1})`;
        if (isAlive(party[index])) {
            document.querySelector(selector).style.visibility = "visible";
        } else {
            document.querySelector(selector).style.visibility = "hidden";
        }
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();
            displayHp(enemyParty, target, false);

            if (!isAlive(enemyParty[target])) {
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            displayHp(friendParty, target, true);

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

味方への攻撃では第3引数にtrueを、敵への攻撃ではfalseを渡しています。

// 敵のHP表現を更新(画像の非表示)
displayHp(enemyParty, target, false);

// 味方のHP表現を更新(HP数値の表示)
displayHp(friendParty, target, true);

これまで、敵の画像の非表示化はif (!isAlive(enemyParty[target])){ }の中で行ってきました。しかし、displayHpの呼び出しはそのif文の前に行っていることに注目してください。

displayHpは内部でisAliveを判定するので、if (!isAlive(enemyParty[target]))の外側で呼び出しても問題ありません。これで、味方の攻撃と敵の攻撃で同じタイミングでHP表示を更新するようになりました。

戦闘の終了判定

ここまでのコードには終了条件がなく、while (true)による無限ループになっています。敵を全員倒しても、味方が全滅しても、戦闘がずっと続いてしまいます。

戦闘を終了させるには、「敵が全滅したか」「味方が全滅したか」を判定する必要があります。

isWipedOut関数

パーティが全滅したかどうかを判定するisWipedOut関数を作りましょう。

// selection:68-75
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index, isFriend) {
    if (isFriend) {
        const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
        document.querySelector(selector).textContent = `HP ${party[index].hp}`;
    } else {
        const selector = `#monster img:nth-child(${index + 1})`;
        if (isAlive(party[index])) {
            document.querySelector(selector).style.visibility = "visible";
        } else {
            document.querySelector(selector).style.visibility = "hidden";
        }
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

function isWipedOut(party) {
    for (const character of party) {
        if (isAlive(character)) {
            return false;
        }
    }
    return true;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();
            displayHp(enemyParty, target, false);

            if (!isAlive(enemyParty[target])) {
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            displayHp(friendParty, target, true);

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
        }
    }
}

main();

この関数は、パーティの中に1人でも生きているキャラクターがいればfalseを返し、全員が倒れていればtrueを返します。

ラベル付きbreak

isWipedOut関数を使って戦闘を終了させたいのですが、ここで問題があります。

今のコードはwhileループの中にforループがあります。forループの中で敵を倒したとき、breakを使うとforループだけを抜け、外側のwhileループは続いてしまいます。

while (true) {                              // ← ここを抜けたい
    for (const character of friendParty) {
        // ...
        if (isWipedOut(enemyParty)) {
            break;                          // ← for...ofループしか抜けない
        }
    }
    // ...
}

この問題を解決するために、JavaScriptにはラベル付きbreakという構文があります。

ループにラベルを付けると、break ラベル名;でそのラベルのループを直接抜けることができます。

// highlight:1,5
battle: while (true) {
    for (const character of friendParty) {
        // ...
        if (isWipedOut(enemyParty)) {
            break battle;                   // ← whileループを抜ける
        }
    }
    // ...
}

battle:というラベルをwhileループに付け、break battle;で直接whileループを抜けています。

コードに適用する

isWipedOut関数とラベル付きbreakを使って、戦闘を終了できるようにしましょう。

// battle.js selection:97-151 highlight:97,120-122,147-149
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index, isFriend) {
    if (isFriend) {
        const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
        document.querySelector(selector).textContent = `HP ${party[index].hp}`;
    } else {
        const selector = `#monster img:nth-child(${index + 1})`;
        if (isAlive(party[index])) {
            document.querySelector(selector).style.visibility = "visible";
        } else {
            document.querySelector(selector).style.visibility = "hidden";
        }
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

function isWipedOut(party) {
    for (const character of party) {
        if (isAlive(character)) {
            return false;
        }
    }
    return true;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    battle: while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();
            displayHp(enemyParty, target, false);

            if (!isAlive(enemyParty[target])) {
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
            if (isWipedOut(enemyParty)) {
                break battle;
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            displayHp(friendParty, target, true);

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
            if (isWipedOut(friendParty)) {
                break battle;
            }
        }
    }
}

main();

displayDeadMessageif文の直後に、パーティが全滅したかを判定するif文を追加しています。全滅していたらbreak battle;whileループを抜けて戦闘を終了します。

プレビューで確認すると、敵または味方が全滅したときに戦闘が終了するようになりました。

試してみよう

これまでの成果

<!-- battle.html folded -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>バトル</title>
    <link rel="stylesheet" href="css/battle.css">
    <script src="js/battle.js" defer></script>
</head>
<body>
    <table id="status">
        <tr>
            <th>ゆうしゃ</th><th>せんし</th><th>そうりょ</th><th>まほうつかい</th>
        </tr>
        <tr><td>HP 153</td><td>HP 198</td><td>HP 101</td><td>HP 77</td></tr>
        <tr><td>MP 25</td><td>MP 0</td><td>MP 35</td><td>MP 58</td></tr>
    </table>
    <div id="monster">
        <img class="dark-knight" src="img/dark-knight.png">
        <img class="dark-lord" src="img/dark-lord.png">
        <img class="demon-priest" src="img/demon-priest.png">
    </div>
    <div id="message">
        まおうがあらわれた。
    </div>
</body>
</html>
/* battle.css folded */
body {
    background-color: rgb(34, 34, 34);
    color: white;
    font-family: sans-serif;
}
table#status {
    border: solid 2px white;
    border-collapse: collapse;
    margin: 10px auto 0 auto;
    width: 640px;
}
table#status tr:first-child {
    border-bottom: solid 1px white;
}
table#status td {
    text-align: center;
}
#monster {
    text-align: center;
    margin-top: 40px;
}
#monster .dark-lord {
    width: 400px;
}
#monster .dark-knight {
    width: 150px;
}
#monster .demon-priest {
    width: 150px;
}
#message {
    border: solid 2px white;
    border-radius: 4px;
    padding: 10px;
    width: 720px;
    margin: 30px auto;
}
// battle.js
function sleep() {
    return new Promise(resolve => setTimeout(resolve, 1000));
}

function calculateDamage(attack, defense) {
    let damage = Math.floor((attack - defense) / 2);
    if (damage < 0) {
        damage = 0;
    }
    return damage;
}

function calculateHp(hp, damage) {
    hp = hp - damage;
    if (hp < 0) {
        hp = 0;
    }
    return hp;
}

function displayMessage(message) {
    document.getElementById("message").textContent = message;
}

function displayDamageMessage(name, damage) {
    if (damage === 0) {
        displayMessage(`${name}にダメージをあたえられない。`);
    } else {
        displayMessage(`${name}${damage}のダメージ。`);
    }
}

function isAlive(character) {
    return character.hp > 0;
}

function displayDeadMessage(name, isFriend) {
    if (isFriend) {
        displayMessage(`${name}はしんでしまった。`);
    } else {
        displayMessage(`${name}をやっつけた。`);
    }
}

function displayHp(party, index, isFriend) {
    if (isFriend) {
        const selector = `#status tr:nth-child(2) td:nth-child(${index + 1})`;
        document.querySelector(selector).textContent = `HP ${party[index].hp}`;
    } else {
        const selector = `#monster img:nth-child(${index + 1})`;
        if (isAlive(party[index])) {
            document.querySelector(selector).style.visibility = "visible";
        } else {
            document.querySelector(selector).style.visibility = "hidden";
        }
    }
}

function selectTarget(party) {
    for (let i = 0; i < party.length; i++) {
        if (isAlive(party[i])) {
            return i;
        }
    }
    return 0;
}

function isWipedOut(party) {
    for (const character of party) {
        if (isAlive(character)) {
            return false;
        }
    }
    return true;
}

async function main() {
    const friendParty = [
        { name: "ゆうしゃ", maxHp: 153, attack: 162, defense: 97 },
        { name: "せんし", maxHp: 198, attack: 178, defense: 111 },
        { name: "そうりょ", maxHp: 101, attack: 76, defense: 55 },
        { name: "まほうつかい", maxHp: 77, attack: 60, defense: 57 }
    ];
    for (const character of friendParty) {
        character.hp = character.maxHp;
    }

    const enemyParty = [
        { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
        { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
        { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
    ];
    for (const character of enemyParty) {
        character.hp = character.maxHp;
    }

    battle: while (true) {
        for (const character of friendParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(enemyParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[target])) {
                displayMessage(`${enemyParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, enemyParty[target].defense);
            enemyParty[target].hp = calculateHp(enemyParty[target].hp, damage);
            displayDamageMessage(enemyParty[target].name, damage);
            await sleep();
            displayHp(enemyParty, target, false);

            if (!isAlive(enemyParty[target])) {
                displayDeadMessage(enemyParty[target].name, false);
                await sleep();
            }
            if (isWipedOut(enemyParty)) {
                break battle;
            }
        }

        for (const character of enemyParty) {
            if (!isAlive(character)) {
                continue;
            }
            const target = selectTarget(friendParty);
            displayMessage(`${character.name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[target])) {
                displayMessage(`${friendParty[target].name}はすでにしんでいる。`);
                await sleep();
                continue;
            }
            let damage = calculateDamage(character.attack, friendParty[target].defense);
            friendParty[target].hp = calculateHp(friendParty[target].hp, damage);
            displayDamageMessage(friendParty[target].name, damage);
            await sleep();
            displayHp(friendParty, target, true);

            if (!isAlive(friendParty[target])) {
                displayDeadMessage(friendParty[target].name, true);
                await sleep();
            }
            if (isWipedOut(friendParty)) {
                break battle;
            }
        }
    }
}

main();