21. ネストされたArray

このレッスンでは、Arrayの中にArrayを入れるネストされたArrayについて学びます。

重複したコード

前回のレッスンで、味方パーティと敵パーティの攻撃ループを作りました。2つのループを見比べてみましょう。

<!-- 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:97-151 highlight:98-123,125-150
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();

2つのループはほとんど同じ構造です。違いは以下の点だけです。

このような重複したコードは、修正時に忘れず両方を変更する必要があるため、修正漏れによるバグを生みやすいです。これを1つにまとめられないでしょうか?もし、friendPartyenemyPartyをArrayとして、parties[0], parties[1]のように扱えればループでなんとかできそうです。

しかし、friendPartyenemyPartyは、それぞれがすでにArrayです。

// battle.js selection:78-95 highlight:78-83,88-92
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();

ArrayをさらにArrayに入れることができるのでしょうか?それがネストされたArrayです。

ネストされたArray

レッスン17では、Arrayの要素に数値やオブジェクトを入れました。同じように、Arrayの要素にはArrayも入れることができます。

const a = [
    [1, 2, 3],
    [4, 5, 6]
];

この例では、aは2個の要素を持つArrayです。a[0][1, 2, 3]a[1][4, 5, 6]です。

要素へのアクセス

ネストされたArrayの要素には、[外側のインデックス][内側のインデックス]の形式でアクセスできます。

const a = [
    [1, 2, 3],
    [4, 5, 6]
];
console.log(a[0]);      // [1, 2, 3](最初のArray)
console.log(a[0][0]);   // 1
console.log(a[0][2]);   // 3
console.log(a[1][0]);   // 4
console.log(a[1][2]);   // 6

a[0][0]の最初のa[0]は、aの0番目の要素である[1, 2, 3]を返します。さらに[0]を付けると、[1, 2, 3]の0番目の要素である1にアクセスできます。

partiesで味方と敵をまとめる

ネストされたArrayを使って、friendPartyenemyPartyを1つのpartiesにまとめましょう。

// battle.js selection:78-96 highlight:78-90,92-96
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 parties = [
        [
            { 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 }
        ],
        [
            { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
            { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
            { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
        ]
    ];

    for (const party of parties) {
        for (const character of party) {
            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();

partiesは2個の要素を持つArrayです。

個別のキャラクターにアクセスするには、parties[0][0]のようにインデックスを2つ指定します。

HPの初期化も、二重のfor…ofループで書き換えています。外側のループで各パーティを、内側のループで各キャラクターを処理します。

余りを使って相手パーティを取得する

攻撃ループを1つにまとめるには、「攻撃側のパーティ」から「相手側のパーティ」を取得する方法が必要です。

ここで、レッスン10で学んだ余り演算子%が役立ちます。

余りの復習

余り演算子%は、割り算の余りを求めます。

console.log(10 % 3);  // 1(10 ÷ 3 = 3 余り 1)
console.log(7 % 2);   // 1(7 ÷ 2 = 3 余り 1)
console.log(6 % 2);   // 0(6 ÷ 2 = 3 余り 0)

2で割った余りは0か1しかありません。これを利用して、0と1を交互に切り替えることができます。

console.log(0 % 2);  // 0
console.log(1 % 2);  // 1
console.log(2 % 2);  // 0
console.log(3 % 2);  // 1

(i + 1) % 2で相手のインデックスを計算

味方パーティはparties[0]、敵パーティはparties[1]です。(i + 1) % 2を使うと、相手パーティのインデックスを計算できます。

i(攻撃側) (i + 1) % 2 意味
0 1 味方(0)のとき、相手は敵(1)
1 0 敵(1)のとき、相手は味方(0)

これを使って、相手パーティを取得できます。

const opposingParty = parties[(i + 1) % 2];

攻撃対象が味方かどうかを判定する

displayHpdisplayDeadMessageは、第3引数に「攻撃を受けた側が味方かどうか」を渡します。

攻撃側が味方(i === 0)のとき、攻撃を受けるのは敵なのでfalseを渡します。攻撃側が敵(i === 1)のとき、攻撃を受けるのは味方なのでtrueを渡します。

i(攻撃側) i % 2 !== 0 攻撃を受けた側
0 false
1 true 味方

i % 2 !== 0で攻撃を受けた側が味方かどうかを判定できます。

const isTargetFriend = i % 2 !== 0;

攻撃ループの統合

これで、2つの攻撃ループを1つにまとめる準備ができました。

// battle.js selection:97-140 highlight:98,99-100,105,127,130
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 parties = [
        [
            { 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 }
        ],
        [
            { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
            { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
            { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
        ]
    ];

    for (const party of parties) {
        for (const character of party) {
            character.hp = character.maxHp;
        }
    }

    battle: while (true) {
        for (let i = 0; i < parties.length; i++) {
            const party = parties[i];
            const opposingParty = parties[(i + 1) % 2];

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

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

main();

ポイントを確認しましょう。

for (let i = 0; i < parties.length; i++)で、味方(i = 0)と敵(i = 1)のパーティを順番に処理します。for…of文ではなくfor文を使っているのは、インデックスiを使って相手パーティやisTargetFriendを計算するためです。

const opposingParty = parties[(i + 1) % 2]で相手パーティを取得しています。

const isTargetFriend = i % 2 !== 0で、攻撃を受けた側が味方かどうかを判定しています。

displayHpdisplayDeadMessageにはisTargetFriendを渡しています。これにより、味方が攻撃を受けたときはHP表示を更新し、敵が攻撃を受けたときは画像を非表示にします。

プレビューで動作を確認してみましょう。これまでと同じように戦闘が進行するはずです。

試してみよう

これまでの成果

<!-- 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 parties = [
        [
            { 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 }
        ],
        [
            { name: "あんこくきし", maxHp: 250, attack: 181, defense: 93 },
            { name: "まおう", maxHp: 999, attack: 186, defense: 58 },
            { name: "デモンプリースト", maxHp: 180, attack: 121, defense: 55 }
        ]
    ];

    for (const party of parties) {
        for (const character of party) {
            character.hp = character.maxHp;
        }
    }

    battle: while (true) {
        for (let i = 0; i < parties.length; i++) {
            const party = parties[i];
            const opposingParty = parties[(i + 1) % 2];

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

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

main();