16. boolean

このレッスンでは、条件式の結果として得られるboolean(論理値)について学びます。

hp > 0 の重複

前回のレッスンで、ゆうしゃとまおうをオブジェクトにまとめました。コードを見返すと、hp > 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:50-76 highlight:50,58,72
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 hero = {
        name: "ゆうしゃ",
        maxHp: 153,
        attack: 162,
        defense: 97
    };
    hero.hp = hero.maxHp;

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

    while (hero.hp > 0 && darkLord.hp > 0) {
        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 (darkLord.hp <= 0) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayMessage(`${darkLord.name}をやっつけた。`);
            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 (hero.hp <= 0) {
            displayMessage(`${hero.name}はしんでしまった。`);
        }
    }
}

main();

「HPが0より大きいかどうか」=「生きているかどうか」という判定が繰り返し出てきます。この判定を関数にまとめられないでしょうか?

isAlive関数

「生きているかどうか」を判定するisAlive関数を作ってみましょう。

// battle.js selection:33-35 highlight:33-35
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;
}

async function main() {
    const hero = {
        name: "ゆうしゃ",
        maxHp: 153,
        attack: 162,
        defense: 97
    };
    hero.hp = hero.maxHp;

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

    while (hero.hp > 0 && darkLord.hp > 0) {
        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 (darkLord.hp <= 0) {
            document.querySelector("#monster img:nth-child(2)").style.visibility = "hidden";
            displayMessage(`${darkLord.name}をやっつけた。`);
            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 (hero.hp <= 0) {
            displayMessage(`${hero.name}はしんでしまった。`);
        }
    }
}

main();

この関数はcharacter(キャラクターのオブジェクト)を受け取り、character.hp > 0の結果を返します。

is命名規則

関数名をisAlive(「生きている」)としました。これは”The character is alive.”(そのキャラクターは生きている)という英文に由来します。

このように、「〇〇は△△である」という意味を持つ関数は、慣例的にisで始める命名が使われます。

boolean(論理値)

isAlive関数が返すcharacter.hp > 0の結果は、どのような値でしょうか?

hp > 0のような比較式は、四則演算と同じように計算できます。四則演算では数値の結果が得られますが、比較式ではtrueまたはfalseという値が得られます。

console.log(10 > 5);    // true
console.log(3 > 7);     // false
console.log(5 === 5);   // true
console.log(5 === 3);   // false

このtrue(真)とfalse(偽)の2つの値をboolean(論理値)と呼びます。

比較演算子

四則演算に使う+-演算子と呼ぶように、比較に使う===>比較演算子と呼びます。

演算子 意味 結果
=== 等しい 5 === 5 true
!== 等しくない 5 !== 3 true
> より大きい 10 > 5 true
< より小さい 3 < 7 true
>= 以上 5 >= 5 true
<= 以下 3 <= 5 true

booleanリテラル

truefalseは、booleanの値をそのまま書いたリテラルです。数値の10や文字列の"hello"と同じように、コードに直接書くことができます。

booleanを変数に格納する

booleanも他の値と同じように、変数に格納できます。

const a = 10 > 5;      // 比較式の結果を格納
console.log(a);        // true

const b = true;        // リテラルを直接格納
const c = false;

const isPlaying = true;
const isGameOver = false;

比較式の結果を変数に格納しておけば、後で何度も使い回すことができます。

ifとwhileの条件

ここで、if文とwhile文を振り返ってみましょう。これまで「条件」と呼んでいた( )の中身は、実はboolean値です。

if (hp > 0) {
    // hp > 0 が true のときに実行される
}

while (hp > 0) {
    // hp > 0 が true の間、繰り返される
}

hp > 0という比較式が計算されてtrueまたはfalseになり、その結果に応じて処理が分岐したりループしたりします。

つまり、boolean値を返す関数や変数をそのまま条件として使えます。

if (isAlive(hero)) {
    // isAlive(hero) が true のときに実行される
}

const isHeroAlive = isAlive(hero);
if (isHeroAlive) {
    // isHeroAlive が true のときに実行される
}

論理演算子

booleanには、専用の演算子があります。

&&(AND演算子)

「かつ」を意味し、両方がtrueのときだけtrueになります。

console.log(true && true);    // true
console.log(true && false);   // false
console.log(false && true);   // false
console.log(false && false);  // false

||(OR演算子)

「または」を意味し、どちらかがtrueならtrueになります。

console.log(true || true);    // true
console.log(true || false);   // true
console.log(false || true);   // true
console.log(false || false);  // false

!(NOT演算子)

「否定」を意味し、truefalseを反転させます。

console.log(!true);   // false
console.log(!false);  // true

演算子の優先順位

論理演算子には優先順位があります。! > && > || の順に優先されます。

// !が最優先
console.log(!true && false);   // false(!true が先に計算され false && false)
console.log(!false || true);   // true(!false が先に計算され true || true)

// &&が||より優先
console.log(true || false && false);   // true(false && false が先に計算され true || false)

四則演算と同様に、()で優先順位を変えられます。

console.log((true || false) && false);  // false(true || false が先に計算され true && false)

コードの書き換え

isAlive関数を使って、バトルのコードを書き換えましょう。

// battle.js selection:54-79 highlight:54,62,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;
}

async function main() {
    const hero = {
        name: "ゆうしゃ",
        maxHp: 153,
        attack: 162,
        defense: 97
    };
    hero.hp = hero.maxHp;

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

    while (isAlive(hero) && isAlive(darkLord)) {
        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";
            displayMessage(`${darkLord.name}をやっつけた。`);
            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)) {
            displayMessage(`${hero.name}はしんでしまった。`);
        }
    }
}

main();

isAlive(hero)は「ゆうしゃが生きている」、!isAlive(darkLord)は「まおうが生きていない」と読め、コードの意図が分かりやすくなりました。

displayDeadMessage関数

次に、死亡メッセージの表示を関数にまとめてみましょう。

現在、まおうが倒れたときは「〇〇をやっつけた。」、ゆうしゃが倒れたときは「〇〇はしんでしまった。」と表示しています。どちらも「キャラクターが倒れた」という状況ですが、味方か敵かでメッセージが異なります。

booleanを引数に取る関数

「味方かどうか」をbooleanで受け取る関数を作ります。

// battle.js selection:37-43 highlight:37-43
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 darkLord = {
        name: "まおう",
        maxHp: 999,
        attack: 186,
        defense: 58
    };
    darkLord.hp = darkLord.maxHp;

    while (isAlive(hero) && isAlive(darkLord)) {
        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";
            displayMessage(`${darkLord.name}をやっつけた。`);
            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)) {
            displayMessage(`${hero.name}はしんでしまった。`);
        }
    }
}

main();

isFriendは「味方かどうか」を表すbooleanの引数です。trueなら味方、falseなら敵です。

booleanを入れる変数や引数も、isAliveと同様にisで始める命名がよく使われます。

コードの書き換え

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

// battle.js selection:62-87 highlight:72,85
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 darkLord = {
        name: "まおう",
        maxHp: 999,
        attack: 186,
        defense: 58
    };
    darkLord.hp = darkLord.maxHp;

    while (isAlive(hero) && isAlive(darkLord)) {
        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(`${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);
        }
    }
}

main();

while (true) と無限ループ

while文の条件にtrueを書くと、条件が常にtrueのため、永遠にループし続けます。これを無限ループと呼びます。

while (true) {
    // このブロックは永遠に繰り返される
}

無限ループを終了するには、break文を使います。

コードの書き換え

現在のコードでは、whileの条件で「両者が生きている」をチェックし、ループ内でも「倒れたか」をチェックしています。これをwhile (true)breakを使って整理しましょう。

まおうの攻撃後もゆうしゃが倒れたらbreakでループを抜けるようにします。

// battle.js selection:62-88 highlight:62,86
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 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(`${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 (true)で無限ループを作り、どちらかが倒れたタイミングで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}のダメージ。`);
    }
}

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 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(`${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();