14. while文

このレッスンでは、同じ処理を繰り返し実行するwhile文について学びます。

複数ターン

RPGのバトルは1ターンでは終わりません。では、2ターン進めるには同じ処理をもう1ターン分書かないといけないのでしょうか?

<!-- 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:29-61 highlight:52-60
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 main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    // 1ターン目
    let damage = calculateDamage(attack1, defense2);
    hp2 = calculateHp(hp2, damage);
    displayDamageMessage(name2, damage);

    damage = calculateDamage(attack2, defense1);
    hp1 = calculateHp(hp1, damage);
    displayDamageMessage(name1, damage);
    document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;

    // 2ターン目
    damage = calculateDamage(attack1, defense2);
    hp2 = calculateHp(hp2, damage);
    displayDamageMessage(name2, damage);

    damage = calculateDamage(attack2, defense1);
    hp1 = calculateHp(hp1, damage);
    displayDamageMessage(name1, damage);
    document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
}

main();

これではどんどんコードが長くなってしまいますし、キリがありません。繰り返し同じ処理をするにはもっと良い方法があります。

while文

繰り返しに使えるのがwhileです。whileifに似ていますが、ifが条件を満たしたら{ }の中を1回実行するのに対して、whileは条件を満たしている間{ }の中を繰り返し実行し続けます。

if (条件) {
    // 条件を満たしたら1回だけ実行
}
while (条件) {
    // 条件を満たしている間は繰り返し実行
}

「ゆうしゃ」が生きている間はバトルを続けるなら、次のように書けます。

while (hp1 > 0) {
    // 「ゆうしゃ」が生きている間は繰り返し実行
}

しかし、バトルは「ゆうしゃ」が死んでしまったときだけでなく、「まおう」を倒しても終わります。つまり、hp1 > 0かつhp2 > 0のときにバトルが続くのです。

hp1 > 0かつhp2 > 0」という条件は、JavaScriptではhp1 > 0 && hp2 > 0と書くことができます。

while (hp1 > 0 && hp2 > 0) {
    // 「ゆうしゃ」も「まおう」も生きている間は繰り返し実行
}

どちらかが勝つまでバトルを続ける

1ターンの処理をwhileで包んでみましょう。

// battle.js selection:29-52 highlight:42,51
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 main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);

        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

これで、どちらかが勝つまでバトルが続けられます。

「ゆうしゃ」の攻撃で「まおう」を倒した場合

今のままだと、1ターンの処理は「ゆうしゃ」が攻撃をしてから「まおう」が攻撃をして終わります。その間にチェックはないので、たとえ「ゆうしゃ」の攻撃で「まおう」を倒しても、「まおう」に攻撃されてしまいます。

これはまずいので、「ゆうしゃ」の攻撃で「まおう」を倒した場合、即座にバトルが終わるようにします。ループの途中でループを終了するにはbreakを使います。

// battle.js selection:42-55 highlight:47-49
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 main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);

        if (hp2 <= 0) { // 「まおう」を倒した場合
            break;
        }

        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

少しずつ進める

今のままだと、バトルの終わりまで一気に進んでしまっておもしろくありません。「ゆうしゃ」や「まおう」が攻撃する度に1秒間待つようにしましょう。

まずは1秒間待つための関数を追加します。今の段階でこのコードの意味を理解するのは困難なので、コピーしてそのまま貼り付けて構いません。

// battle.js selection:1-3 highlight:1-3
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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            break;
        }

        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

この関数の呼び出し方は普通と異なります。単にsleep()と書くのではなく、await sleep()のようにawaitを付けて呼び出します。

これを使えるようにするためには、main関数を少し変更する必要があります。function main()async function main()に変えます。

// battle.js selection:33-33 highlight:33
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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            break;
        }

        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

では、「ゆうしゃ」と「まおう」の行動の後でawait sleep();して1秒間ずつ待つようにしましょう。

// battle.js selection:46-61 highlight:50,59
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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            break;
        }

        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

攻撃メッセージ

今のままだと、いきなりダメージが表示されて誰の攻撃なのかわかりにくいですね。攻撃の前に「○○のこうげき。」と表示するようにしましょう。

すでに定義済みのdisplayMessage関数を使えば、メッセージを表示できます。攻撃の前にメッセージを表示し、await sleep()で1秒待ってからダメージを計算するようにします。

// battle.js selection:46-69 highlight:47-48,58-59
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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        displayMessage(`${name1}のこうげき。`);
        await sleep();
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            break;
        }

        displayMessage(`${name2}のこうげき。`);
        await sleep();
        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;
    }
}

main();

これで、「ゆうしゃのこうげき。」→(1秒待つ)→「まおうに52のダメージ。」→(1秒待つ)→「まおうのこうげき。」→…という流れになります。

バトルの結果

最後に、「ゆうしゃ」が勝った場合には「まおうをやっつけた。」、「まおう」が勝った場合には「ゆうしゃはしんでしまった。」と表示しましょう。

勝敗が決まるのは相手のHPが0以下になったときです。すでに「ゆうしゃ」の攻撃後にhp2 <= 0のチェックがあるので、ここで「まおうをやっつけた。」と表示してからbreakしましょう。「まおう」の攻撃後も同様に、HPが0以下になったら「ゆうしゃはしんでしまった。」と表示します。

また、「まおう」を倒したときは敵の画像を非表示にしましょう。document.querySelector("#monster img:nth-child(2)")で「まおう」の画像要素を取得し、style.visibility = "hidden"で非表示にします。メッセージを表示する前に画像を非表示にすることで、より自然な演出になります。

// battle.js selection:46-74 highlight:55-56,69
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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        displayMessage(`${name1}のこうげき。`);
        await sleep();
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayMessage(`${name2}をやっつけた。`);
            break;
        }

        displayMessage(`${name2}のこうげき。`);
        await sleep();
        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;

        if (hp1 <= 0) {
            displayMessage(`${name1}はしんでしまった。`);
        }
    }
}

main();

「まおう」の攻撃の場合は、「ゆうしゃ」が死んでしまった場合でもbreakしていません。これは、直後にwhileループの冒頭に戻り、hp1 > 0 && hp2 > 0のチェックを受けてループを抜けるからです。もちろん、「まおう」の攻撃のときもbreakを書いても問題はありません。

試してみよう

これまでの成果

<!-- 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}のダメージ。`);
    }
}

async function main() {
    const name1 = "ゆうしゃ";
    const maxHp1 = 153;
    let hp1 = maxHp1;
    const attack1 = 162;
    const defense1 = 97;

    const name2 = "まおう";
    const maxHp2 = 999;
    let hp2 = maxHp2;
    const attack2 = 186;
    const defense2 = 58;

    while (hp1 > 0 && hp2 > 0) {
        displayMessage(`${name1}のこうげき。`);
        await sleep();
        let damage = calculateDamage(attack1, defense2);
        hp2 = calculateHp(hp2, damage);
        displayDamageMessage(name2, damage);
        await sleep();

        if (hp2 <= 0) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayMessage(`${name2}をやっつけた。`);
            break;
        }

        displayMessage(`${name2}のこうげき。`);
        await sleep();
        damage = calculateDamage(attack2, defense1);
        hp1 = calculateHp(hp1, damage);
        displayDamageMessage(name1, damage);
        await sleep();
        document.querySelector("#status tr:nth-child(2) td:nth-child(1)").textContent = `HP ${hp1}`;

        if (hp1 <= 0) {
            displayMessage(`${name1}はしんでしまった。`);
        }
    }
}

main();