17. Array

このレッスンでは、複数のデータをまとめて管理するArray(配列)について学びます。

「せんし」を追加する

前回のレッスンでは、isAlive関数やdisplayDeadMessage関数を作り、booleanを活用してコードを整理しました。今度は「せんし」を追加してみましょう。

<!-- 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:46-109 highlight:54-60,84-95
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 hero = {
        name: "ゆうしゃ",
        maxHp: 153,
        attack: 162,
        defense: 97
    };
    hero.hp = hero.maxHp;

    const warrior = {
        name: "せんし",
        maxHp: 198,
        attack: 178,
        defense: 111
    };
    warrior.hp = warrior.maxHp;

    const darkLord = {
        name: "まおう",
        maxHp: 999,
        attack: 186,
        defense: 58
    };
    darkLord.hp = darkLord.maxHp;

    while (true) {
        displayMessage(`${hero.name}のこうげき。`);
        await sleep();
        let damage = calculateDamage(hero.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${warrior.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(warrior.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

「せんし」を追加して、「ゆうしゃ」→「せんし」→「まおう」の順で攻撃するようになりました。しかし、攻撃の処理が3回も書かれていて、コードが長くなってしまいました。

Arrayの必要性

さらに「そうりょ」や「まほうつかい」を追加すると、同じような攻撃処理が5回になります。敵にも「あんこくきし」や「デモンプリースト」を追加するともっと大変です。

攻撃の処理は似ているので、ループで繰り返し実行できると便利です。しかし、herowarriorのように別々の変数になっていると、ループで扱うことができません。

JavaScriptには、複数のデータをまとめて管理するArray(配列)という仕組みがあります。Arrayを使えば、味方全員をまとめて扱い、ループで処理できるようになります。

Array

Arrayは[ ]で囲んで作ります。

const numbers = [2, 3, 5];

[ ]の中に入れた値を要素と呼びます。要素はカンマ(,)で区切ります。

要素へのアクセス

Arrayの要素には[番号]でアクセスできます。番号は0から始まります。

const numbers = [2, 3, 5];
console.log(numbers[0]);  // 2
console.log(numbers[1]);  // 3
console.log(numbers[2]);  // 5

要素の変更

要素に代入することで、値を変更できます。

const numbers = [2, 3, 5];
numbers[0] = 10;
console.log(numbers[0]);  // 10

lengthプロパティ

Arrayにはlengthプロパティがあり、要素の数を取得できます。

const numbers = [2, 3, 5];
console.log(numbers.length);  // 3

whileループで合計を計算する

lengthとインデックスを使えば、whileループでArrayの全要素を処理できます。

const numbers = [2, 3, 5];
let sum = 0;
let i = 0;
while (i < numbers.length) {
    sum = sum + numbers[i];
    i = i + 1;
}
console.log(sum);  // 10

変数iを0から始めて、numbers.length(3)より小さい間ループします。ループのたびにiを1増やすので、iは0、1、2と変化し、numbers[0]numbers[1]numbers[2]の順にアクセスできます。

インクリメント

上のコードでは、ループのたびにi = i + 1iを1増やしています。変数に1を加える操作をインクリメントと呼びます。

JavaScriptではインクリメントを省略して書く方法がいくつかあります。

+=演算子を使うと、i = i + 1i += 1と書けます。

i = i + 1;  // これと
i += 1;     // これは同じ意味

+=演算子はi += 2などインクリメント以外にも使えますが、インクリメントはよく行うのでもっと短く書く方法が用意されています。それには++演算子を使います。

i += 1;  // これと
i++;     // これは同じ意味

先ほどの合計を計算するコードは、i++を使って次のように書けます。

const numbers = [2, 3, 5];
let sum = 0;
let i = 0;
while (i < numbers.length) {
    sum = sum + numbers[i];
    i++;
}
console.log(sum);  // 10

オブジェクトのArray

Arrayの要素には、数値だけでなくオブジェクトも入れられます。

すでに定義したherowarriorを使って、味方パーティのArrayを作ることができます。

const friendParty = [hero, warrior];

また、Arrayリテラルの中に直接オブジェクトリテラルを書くこともできます。この書き方で、4人の味方パーティを作ってみましょう。

// battle.js selection:46-64 highlight:46-56
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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

    const darkLord = {
        name: "まおう",
        maxHp: 999,
        attack: 186,
        defense: 58
    };
    darkLord.hp = darkLord.maxHp;

    while (true) {
        displayMessage(`${hero.name}のこうげき。`);
        await sleep();
        let damage = calculateDamage(hero.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${warrior.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(warrior.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

friendParty[0]は「ゆうしゃ」のオブジェクト、friendParty[1]は「せんし」のオブジェクトになります。friendParty[0].nameで「ゆうしゃ」の名前にアクセスできます。

Arrayリテラルの直後でwhileループを使って、全員のhpmaxHpで初期化しています。darkLordとバトル処理はまだ古いままですが、この後書き換えていきます。

敵パーティのArray

friendPartyを参考に、敵パーティenemyPartyもArrayにするとどうなるでしょうか?以下のステータスを持つ3体の敵を追加して、hpの初期化も行ってみてください。

名前 最大HP 攻撃力 防御力
あんこくきし 250 181 93
まおう 999 186 58
デモンプリースト 180 121 55
// battle.js highlight:58-67 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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        displayMessage(`${hero.name}のこうげき。`);
        await sleep();
        let damage = calculateDamage(hero.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${warrior.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(warrior.attack, darkLord.defense);
        darkLord.hp = calculateHp(darkLord.hp, damage);
        displayDamageMessage(darkLord.name, damage);
        await sleep();

        if (!isAlive(darkLord)) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayDeadMessage(darkLord.name, false);
            break;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

味方の攻撃をループにする

それでは、味方の攻撃をwhileループで書き換えましょう。

// battle.js selection:71-88 highlight:71-88
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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        i = 0;
        while (i < friendParty.length) {
            if (isAlive(friendParty[i])) {
                displayMessage(`${friendParty[i].name}のこうげき。`);
                await sleep();
                let damage = calculateDamage(friendParty[i].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();
                }
            }
            i++;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

味方全員が順番にenemyParty[0](あんこくきし)を攻撃します。if (isAlive(friendParty[i]))で、生存しているキャラクターだけが攻撃するようにしています。

敵のHPが0以下になったら「〇〇をやっつけた。」と表示します。

continueでネストを浅くする

ところで、今のコードを見ると、ネストが3段階(whilewhileif)になっていて、少し読みづらくなっています。

while (true) {   // 1段目
    while (i < friendParty.length) {                       // 2段目
        if (isAlive(friendParty[i])) {                     // 3段目
            // 攻撃処理...
        }
        i++;
    }
}

このような場合、continueを使うとネストを浅くできます。

continueは、ループの残りの処理をスキップして、次の繰り返しに進む命令です。breakがループを完全に抜けるのに対し、continueは「今回の繰り返しだけスキップ」します。

生存チェックをcontinueで書き換えると、こうなります。

if (!isAlive(friendParty[i])) {
    i++;
    continue;
}

「もし生存していなければ、次のキャラクターへスキップ」という流れです。条件を反転(isAlive!isAlive)している点に注目してください。

これを使うと、味方の攻撃処理は次のように書き換えられます。

// battle.js selection:71-91 highlight:73-76
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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        i = 0;
        while (i < friendParty.length) {
            if (!isAlive(friendParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${friendParty[i].name}のこうげき。`);
            await sleep();
            let damage = calculateDamage(friendParty[i].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();
            }
            i++;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

攻撃処理がifの外に出たので、ネストが1段浅くなりました。

すでに倒れている敵への攻撃

今のコードでは、味方全員がenemyParty[0]を攻撃します。しかし、途中でenemyParty[0]が倒れても、残りの味方は倒れた敵を攻撃し続けてしまいます。

すでに倒れている敵を攻撃しようとした場合は「〇〇のこうげき。」の後に「〇〇はすでにしんでいる。」と表示して、次の味方に順番を回すようにしましょう。これもcontinueを使って実現できます。

// battle.js selection:77-84 highlight:79-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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        i = 0;
        while (i < friendParty.length) {
            if (!isAlive(friendParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${friendParty[i].name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[0])) {
                displayMessage(`${enemyParty[0].name}はすでにしんでいる。`);
                await sleep();
                i++;
                continue;
            }
            let damage = calculateDamage(friendParty[i].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();
            }
            i++;
        }

        displayMessage(`${darkLord.name}のこうげき。`);
        await sleep();
        damage = calculateDamage(darkLord.attack, hero.defense);
        hero.hp = calculateHp(hero.hp, damage);
        displayDamageMessage(hero.name, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hero.hp}`;

        if (!isAlive(hero)) {
            displayDeadMessage(hero.name, true);
            break;
        }
    }
}

main();

敵の攻撃をループにする

同様に、敵の攻撃もwhileループで書き換えましょう。味方の攻撃を参考にやってみてください。

// battle.js highlight:98-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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        i = 0;
        while (i < friendParty.length) {
            if (!isAlive(friendParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${friendParty[i].name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[0])) {
                displayMessage(`${enemyParty[0].name}はすでにしんでいる。`);
                await sleep();
                i++;
                continue;
            }
            let damage = calculateDamage(friendParty[i].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();
            }
            i++;
        }

        i = 0;
        while (i < enemyParty.length) {
            if (!isAlive(enemyParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${enemyParty[i].name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[0])) {
                displayMessage(`${friendParty[0].name}はすでにしんでいる。`);
                await sleep();
                i++;
                continue;
            }
            let damage = calculateDamage(enemyParty[i].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();
            }
            i++;
        }
    }
}

main();

これで、味方4人と敵3人のバトルがArrayとループで動くようになりました。

戦闘の終了

今のコードには終了条件がなく、敵を全員倒しても、味方が全滅しても、戦闘がずっと続いてしまいます。

戦闘を終了させるには、「敵が全滅したか」「味方が全滅したか」を判定する必要があります。しかし、今のコードでは攻撃対象が固定(friendParty[0]およびenemyParty[0])なので、倒れたキャラクターを攻撃し続けてしまい、決着がつきません。

今後のレッスンで、生きているキャラクターを選んで攻撃できるようにした後、終了判定を導入します。今は無限ループのままで構いません。

試してみよう

これまでの成果

<!-- 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}をやっつけた。`);
    }
}

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 }
    ];
    let i = 0;
    while (i < friendParty.length) {
        friendParty[i].hp = friendParty[i].maxHp;
        i++;
    }

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

    while (true) {
        i = 0;
        while (i < friendParty.length) {
            if (!isAlive(friendParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${friendParty[i].name}のこうげき。`);
            await sleep();
            if (!isAlive(enemyParty[0])) {
                displayMessage(`${enemyParty[0].name}はすでにしんでいる。`);
                await sleep();
                i++;
                continue;
            }
            let damage = calculateDamage(friendParty[i].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();
            }
            i++;
        }

        i = 0;
        while (i < enemyParty.length) {
            if (!isAlive(enemyParty[i])) {
                i++;
                continue;
            }
            displayMessage(`${enemyParty[i].name}のこうげき。`);
            await sleep();
            if (!isAlive(friendParty[0])) {
                displayMessage(`${friendParty[0].name}はすでにしんでいる。`);
                await sleep();
                i++;
                continue;
            }
            let damage = calculateDamage(enemyParty[i].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();
            }
            i++;
        }
    }
}

main();