【Html&Cssで作る四択クイズゲーム開発記録④】HTMLのスクリプト部分(Javascript)をfunctionでまとめる


※ 当ページには【広告/PR】を含む場合があります。
2022/01/09
【Html&Cssで作る四択クイズゲーム開発記録③】Sassリストでゲームデータを集約してHTML要素を初期化する
【Html&Cssで作る四択クイズゲーム開発記録⑤】Sassリストでクイズの問題を動的に管理する
合同会社タコスキングダム|TacosKingdom,LLC.

新年明けましておめでとうございます。

2022年も弊社タコスキングダムのPスクールブログの方も宜しくお願いいたします。

では前回に引き続き四択クイズミニゲームの修正・改良を加えていきます。

合同会社タコスキングダム|タコキンのPスクール
【Html&Cssで作る四択クイズゲーム開発記録③】Sassリストでゲームデータを集約してHTML要素を初期化する

Sassリストを高度に使いこなし、HTML擬似要素の内容を初期化する方法を紹介します。

今回のお題は
『javascriptのfunctionをまとめる』ことを中心に解説していきます。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

前回までの課題洗い出し

まずは改良前のHTMLコードの設問部分を良く眺めてみます。

なお、改造前の全コードをはこの
ブログの末尾にある付録で紹介しています。全容が知りたい方はそちらをご覧ください。

            
            //...中略
document.getElementById("stage1").addEventListener("change", (e) => {
    const r_ = document.getElementById("checkmark1").checked;
    if (r_) {
        document.getElementById("gamestate").textContent += '⭕ ';
    } else {
        document.getElementById("gamestate").textContent += '❌ ';
    }
});
document.getElementById("stage2").addEventListener("change", (e) => {
    const r_ = document.getElementById("checkmark2").checked;
    if (r_) {
        document.getElementById("gamestate").textContent += '⭕ ';
    } else {
        document.getElementById("gamestate").textContent += '❌ ';
    }
});
document.getElementById("stage3").addEventListener("change", (e) => {
    const r_ = document.getElementById("checkmark3").checked;
    if (r_) {
        document.getElementById("gamestate").textContent += '⭕ ';
    } else {
        document.getElementById("gamestate").textContent += '❌ ';
    }
    let result = 0;
    document.getElementById("checkmark1").checked && result++;
    document.getElementById("checkmark2").checked && result++;
    document.getElementById("checkmark3").checked && result++;
    document.getElementById("score").textContent = `正解率は ${result}/3 でした。`;
});
//...中略
        
修正前のコードの問題点として、各設問ステージでの正誤判定を行った後の画面の描画処理で、Javascriptスクリプトからchangeイベントハンドラを仕込んでいました。

このままだと力技すぎるので、今回はここからもっとスマートにコード改造に着手してみましょう。

scriptタグ内が散らかっている〜functionでまとめられるメソッドを一本化

現在はたかだか3問ですが、同じ処理をコピペしてステージの名前を変えているだけですので、これは一つの関数として圧縮するという改善の余地があります。

            
            function shiftGamgeState({stageName, checkbox}) {
    document.getElementById(stageName).addEventListener("change", (e) => {
        document.getElementById("gamestate").textContent += document.getElementById(checkbox).checked ? '⭕ ' : '❌ ';
    });
}
[
    {stageName: "stage1", checkbox: "checkmark1"},
    {stageName: "stage2", checkbox: "checkmark2"},
    {stageName: "stage3", checkbox: "checkmark3"},
].forEach(g => shiftGamgeState(g));
        
というようにやるとスッキリして、設問を増やすのも楽になります。

集計処理を独立化する

集計処理も変更します。

コードの修正前では、最後の3番目の設問が終了時に、正誤判定の集計を行う処理もハードコーディングしていました。

            
            document.getElementById("stage3").addEventListener("change", (e) => {
    const r_ = document.getElementById("checkmark3").checked;
    if (r_) {
        document.getElementById("gamestate").textContent += '⭕ ';
    } else {
        document.getElementById("gamestate").textContent += '❌ ';
    }
    //👇集計処理
    let result = 0;
    document.getElementById("checkmark1").checked && result++;
    document.getElementById("checkmark2").checked && result++;
    document.getElementById("checkmark3").checked && result++;
    document.getElementById("score").textContent = `正解率は ${result}/3 でした。`;
});
        
これだと、毎回設問を増やすたびに集計項目を手動で増やさないといけないですし、設問が多くなってくるほどミスが多くなりそうでよろしくありません。

ではどうするかというと、集計するタイミングを測る機能と、集計を行う機能を持ったfunctionを別に作成しておき、先ほどの正誤判定後の描画処理関数に追加します。

            
            //...
function checkGameOver() {
    let isGameOver = true;
    document.querySelectorAll("input[id^='stage']").forEach((checkItem) => {
        isGameOver = isGameOver && checkItem.checked;
    });
    return isGameOver;
}
function checkSum() {
    let result = 0, total = 0;
    document.querySelectorAll("input[id^='checkmark']").forEach((checkItem) => {
        checkItem.checked && result++;
        total++;
    });
    document.getElementById("score").textContent = `正解率は ${result}/${total} でした。`;
}
function shiftGamgeState({stageName, checkbox}) {
    document.getElementById(stageName).addEventListener("change", (e) => {
        document.getElementById("gamestate").textContent += document.getElementById(checkbox).checked ? '⭕ ' : '❌ ';
        checkGameOver() && checkSum();//👈追加
    });
}
//...
        
こうすることで、全ての設問に答えたときに、自動で集計処理が走るようにできます。

結果的に、散らかったソースコードを3つの関数(
checkGameOver / checkSum / shiftGamgeState)で分割して、管理・拡張のしやすいコードに仕上がりました。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

余談〜即時関数で囲う

ブラウザでゲームを作りたい場合、ブラウザの裏で走るプロセスで使われる重要な「グローバル変数」が存在しています。

代表格としては、
windowdocumentなど特別な役割を持ったグローバル変数や、サーバーサイドでバックエンドでNode.jsを使っていれば、processやfsなどもあります。

ということで、Javascriptのコードが増えていくと、ふとしたはずみでこれらの重要なグローバル変数を知らず知らず上書きしてしまったら最後、アプリケーションまともに動かなくなってしまう恐れがあります。

このことはJavascript特有の
「スコープ汚染」と知られ、グローバルスコープへの汚染の危険性を少しでも減らすには、「即時関数」と呼ばれる無名関数の関数スコープの中でプログラムコードの実装を行うテクニックを使うことが推奨されています。

参考サイト|JavaScriptで即時関数を使う理由

使い方はとても簡単で、scriptタグの中で、無名関数を定義して、その中身でコードを実装していきます。

            
            <script>
(function() {
    ///スクリプトの定義
})();
</script>
        
また別パターンで以下のような全体を()で囲うパターンの即時関数も使われますが、スコープ汚染を防ぐという意味合いでは変わりません。

            
            <script>
(function() {
    ///スクリプトの定義
}());
</script>
        
ES6以降であれば、アロー関数を利用した即時関数も最近では良く見かけます。

            
            <script>
(() => {
    ///スクリプトの定義
})();
</script>
        
更にPromiseなどの非同期の関数をawaitで使う必要があれば、即時関数をasyncで予め記述して使うこともできます。

            
            <script>
(async () => {
    ///スクリプトの定義(非同期処理を含む)
})();
</script>
        
ひと手間ではありますが、とりあえず即時関数を仕込んでおくと、そのあとのHTMLアプリ開発も少しは安心できるかも知れません。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

まとめ

今回はfunctionを使うことで、同じような処理を行うメソッドを統合するところまで行いました。

このHTML&CSSミニゲーム作成シリーズも思い付きでやってみておりますので、四択クイズゲームとして遊べるようになるまでもう少しかかりそうな気配です...。

あと数回でHTMLの基礎的な文法の話を挟むかも知れませんが、完成まであとしばらくお付き合いください。

付録〜改良前のソースコード(バックアップ)

今回も恒例の古くなったソースコード(バージョン0.4)のバックアップを付録として掲載しておきます。

index.html

            
            <!DOCTYPE html>
<html lang="ja">
<html>
    <meta charset="utf-8"/>
    <head>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <form id="game-wrapper">
            <p class="game-header"><span id="gamestate">さあ4択クイズを始めましょう</span></p>
            <input type="reset" id="reset"/>
            <input type="checkbox" id="start"/>
            <input type="checkbox" id="stage1"/>
            <input type="checkbox" id="stage2"/>
            <input type="checkbox" id="stage3"/>
            <input type="checkbox" id="checkmark1"/>
            <input type="checkbox" id="checkmark2"/>
            <input type="checkbox" id="checkmark3"/>
            <div id="stage---op" class="stage stage--op">
                <label for="start">クリックしてスタート</label>
            </div>
            <div id="stage---1" class="stage stage--main stage--main--1">
                <label for="stage1"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---2" class="stage stage--main stage--main--2">
                <label for="stage2"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---3" class="stage stage--main stage--main--3">
                <label for="stage3"><ul class="flexlist"><li><li><li><li>
            </div>
            <div id="stage---reset" class="stage stage--end">
                <p class="score-board"><span id="score"></p>
                <label for="reset">もう一度トライ</label>
            </div>
        </form>
        <script>
            document.getElementById("start").addEventListener("change", (e) => {
                document.getElementById("gamestate").textContent = '成績: ';
            });
            document.getElementById("stage1").addEventListener("change", (e) => {
                const r_ = document.getElementById("checkmark1").checked;
                if (r_) {
                    document.getElementById("gamestate").textContent += '⭕ ';
                } else {
                    document.getElementById("gamestate").textContent += '❌ ';
                }
            });
            document.getElementById("stage2").addEventListener("change", (e) => {
                const r_ = document.getElementById("checkmark2").checked;
                if (r_) {
                    document.getElementById("gamestate").textContent += '⭕ ';
                } else {
                    document.getElementById("gamestate").textContent += '❌ ';
                }
            });
            document.getElementById("stage3").addEventListener("change", (e) => {
                const r_ = document.getElementById("checkmark3").checked;
                if (r_) {
                    document.getElementById("gamestate").textContent += '⭕ ';
                } else {
                    document.getElementById("gamestate").textContent += '❌ ';
                }
                let result = 0;
                document.getElementById("checkmark1").checked && result++;
                document.getElementById("checkmark2").checked && result++;
                document.getElementById("checkmark3").checked && result++;
                document.getElementById("score").textContent = `正解率は ${result}/3 でした。`;
            });
            document.getElementById("reset").addEventListener("click", (e) => {
                document.getElementById("gamestate").textContent = 'さあ4択クイズを始めましょう';
            });
            document.querySelectorAll("ul.flexlist > li").forEach((userItem) => {
                userItem.addEventListener("click", (e) => {
                    const styles = window.getComputedStyle(e.target,'::after')
                    const content = styles['content'];
                    const m_ = content.match(/(\d+)\/correct/);
                    if (m_) {
                        document.getElementById("checkmark" + m_[1]).checked = true;
                    };
                });
            });
        </script>
    </body>
</html>
        

styles.scss

            
            #game-wrapper {
    width: 100%;
    height: 300px;
    background-color: darkgray;
    box-sizing: border-box;
    position:relative;
    input { display: none; }
    .game-header {
        position: absolute;
        top: 0;
        left: 0;
        background: #220c0c;
        color: #ceeece;
        z-index: 1;
        margin: 0;
        font-size: 22px;
        padding: 6px 0 6px 0;
        width: 100%;
    }
    .stage {
        position: absolute;
        display: block;
        width: 100%;
        height: 100%;
        font-size: 24px;
        &--op {
            display: flex;
            align-items: center;
            justify-content: center;
            label {
                display: block;
                flex: 0 0 auto;
                margin: 0 auto;
                animation: flash 1s linear infinite;
            }
        }
        &--main {
            display: flex;
            align-items: center;
            justify-content: center;
            label {
                flex: 0 0 auto;
                margin: 0 auto;
            }
            @each $key, $val in (1:blue, 2:rgb(231, 185, 99), 3:rgb(211, 51, 203)) {
                &--#{$key} {
                    color: $val;
                    font-size: 22px;
                }
            }
        }
        &--end {
            display: flex;
            align-items: center;
            justify-content: center;
            flex-direction: column;
            background: darkgray;
            label {
                flex: 0 0 auto;
                display: inline-block;
                font-weight: bold;
                padding: 0 0 0 0;
                text-decoration: none;
                color: #00BCD4;
                background: #ECECEC;
                margin: 0 auto;
                animation: flash 1s linear infinite;
            }
        }
        @keyframes flash {
            0%,100% {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
        }
    }
    .stage--main {
        ul.flexlist {
            border: 1px solid #666;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            list-style-type: none;
            margin: 10px auto 2px;
            padding: 0;
            width: 300px;
            li {
                border: 1px solid #aaa;
                line-height: 110%;
                margin: 5px 2px 5px 2px;
                padding: 8px;
                text-align: center;
                width: 35%;
                &:hover {
                    color: chartreuse;
                }
                &::after {
                    content: "wrong";
                    visibility: hidden;
                    display: block;
                    width: 0;
                    height: 0;
                }
            }
        }
    }
    #stage {
        &---op {display: flex}
        @each $item in (1, 2, 3, reset) {
            &---#{$item} { display: none; }
        }
    }
    $stages: (
        'start' : ('op', ''),
        'stage1': ('1', 'タコの足は何本?', ('5本', '8本', '10本', '足はない'), 2),
        'stage2': ('2', 'タコは何動物?', ('地球外生物', '緩歩動物', '実は植物', '軟体動物'), 4),
        'stage3': ('3', 'タコの水揚げ量が世界一の国?', ('中国','カナダ','モーリタニア','日本'), 1)
    );
    @for $i from 1 through length($stages) {
        $item: nth($stages, $i);
        $item: nth($item,1);
        ##{$item}:checked ~ {
            $item: nth($stages, $i);
            $item: nth($item, 2);
            $item: nth($item,1);
            #stage---#{$item} {display: none}
            @if $i == length($stages) {
                #stage---reset {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                }
            } @else {
                $item: nth($stages, $i + 1);
                $item: nth($item,2);
                $item: nth($item,1);
                #stage---#{$item} {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                    $item: nth($stages, $i + 1);
                    $item: nth($item,2);
                    $item: nth($item,2);
                    label::before {
                        content: '問題#{$i}:#{$item}';
                    }
                    $item: nth($stages, $i + 1);
                    $item: nth($item,2);
                    $quiz_option: nth($item,3);
                    $answer: nth($item,4);
                    @for $j from 1 through length($quiz_option) {
                        ul.flexlist {
                            li:nth-child(#{$j}) {
                                &::before {content: '#{nth($quiz_option,$j)}';}
                                @if $j == $answer {
                                    &::after { content: "#{nth($item,1)}/correct"; }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    @keyframes fadeIn {
        0% {
            display: none;
            opacity: 0;
        }
        1% {
            display: flex;
            opacity: 0;
        }
        100% {
            display: flex;
            opacity: 1;
        }
    }
}