【Html&Cssで作る四択クイズゲーム開発記録②】擬似要素のcontentプロパティでクイズの正誤判定に使う


※ 当ページには【広告/PR】を含む場合があります。
2021/12/18
【Html&Cssで作る四択クイズゲーム開発記録①】Sassのリストと@forループを使ったコード省力化の話
【Html&Cssで作る四択クイズゲーム開発記録③】Sassリストでゲームデータを集約してHTML要素を初期化する
合同会社タコスキングダム|TacosKingdom,LLC.

SassをベースにHTML&CSSミニゲームを不定期で開発していく企画の第2段です。

前回ではSassのリスト@for(@each)ループを使う効率的なCSSコードの作成ポイントを解説していきました。

合同会社タコスキングダム|タコキンのPスクール
【Html&Cssで作る四択クイズゲーム開発記録①】Sassのリストと@forループを使ったコード省力化の話

Sassの@forループとリストのキーや値の取り出しの使い方を考えてみます。

今回は四択クイズの正誤判定を行う上で、後からクイズ問題を増やして行くことを考慮したときに、より再生産性の高い手法にSassコードを改造してみましょう。


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

改善点の洗い出し

まず改良前の四択問題でどのように正誤判定していたかというところから見ていきます。

※ 改善前のリソース(index.htmlとstyle.scss)のフルパージョンは
この記事の末尾の付録に載せています。

以下は第一問(stage1)の4択部分のlabelブロックを抜粋したものです。

            
            <!-- ...中略 -->
    <label for="stage1">
        タコの足は何本?
        <ul class="flexlist">
            <li>5本</li>
            <li id="correct-option-1">8本</li>
            <li>10本</li>
            <li>足はない</li>
        </ul>
    </label>
<!-- ...中略 -->
        

4つの選択肢のうち、正解には
id="correct-option-1"を付けておいて、スクリプトの箇所で、

            
            <!-- ...中略 -->
<script>
    //...中略
    document.getElementById("correct-option-1").addEventListener("click", (e) => {
        document.getElementById("checkmark1").checked = true;
    });
    //...中略
</script>
<!-- ...中略 -->
        

というように正解の選択肢(
<li>)がクリックされた場合にのみ、隠しチェックボックスにチェックが付くようしています。

でも、問題が少ないうちは良いものの、後で設問を追加していく全てに
id="correct-option-*"とそのクリックイベントを手動で入れていくのは面倒です。

また、コードの管理の面からも好ましいやり方ではありません。

そこで、より
Sass的なソリューションを導入し、効率的に4択クイズの正誤判定をやれるように改造を試みます。

まずは設問のラベルブロックから
id="correct-option-*"をゴッソリと失くします。

            
            <!-- ...中略 -->
    <div id="stage---1" class="stage stage--main stage--main--1">
        <label for="stage1">
            タコの足は何本?
            <ul class="flexlist">
                <li>5本</li>
                <li>8本</li>
                <li>10本</li>
                <li>足はない</li>
            </ul>
        </label>
    </div>
    <div id="stage---2" class="stage stage--main stage--main--2">
        <label for="stage2">
            タコは何動物?
            <ul class="flexlist">
                <li>地球外生物</li>
                <li>緩歩動物</li>
                <li>実は植物</li>
                <li>軟体動物</li>
            </ul>
        </label>
    </div>
    <div id="stage---3" class="stage stage--main stage--main--3">
        <label for="stage3">
            タコの水揚げ量が世界一の国?
            <ul class="flexlist">
                <li>中国</li>
                <li>カナダ</li>
                <li>モーリタニア</li>
                <li>日本</li>
            </ul>
        </label>
    </div>
<!-- ...中略 -->
        

...これでは正解かどれか分からないじゃないか?と思われるかも知れませんが、正誤リストとなるデータを
style.scss側に移します。

とはいえCSSスタイル用に設計されているプロパティの値は、もともとさほど自由度の高い引数を書き込めるようには出来ていません。

ではどのプロパティを使うかというと、
::beforeか::afterの擬似要素のcontentプロパティに正誤判定の文字列を非表示で入れておく、という多少トリッキーなやり方をする必要があります。

具体的には以下のようなコードです。

            
            //...中略
    .stage--main {
        ul.flexlist {
            //...中略
            li {
                //👇after擬似要素のcontentを全て"wrong"(間違い)で初期化
                &::after {
                    content: "wrong";
                    visibility: hidden;
                    display: block;
                    width: 0;
                    height: 0;
                }
            }
        }
    }
    #stage {
        //...中略
        //👇正誤判定用のリストで正解の箇所だけafter擬似要素のcontentを"<設問番号>/correct"にする
        @each $item, $val in ('1':2, '2':4, '3':1) {
            &---#{$item} {
                ul.flexlist {
                    li:nth-child(#{$val}) {
                        &::after { content: "#{$item}/correct"; }
                    }
                }
            }
        }
    }
        
このコード内の('1':2, '2':4, '3':1)のリスト部分が、各問題で何番目の<li>要素が正解なのかを与えるようにしています。

このリストもまだ改善の余地がありますが、とりあえず
設問1は選択肢の2番目、設問2は選択肢の4番目、設問3は選択肢の1番目がそれぞれ答えとなるようにしています。

後はHTMLのスクリプト部分で、このcontentプロパティをコッソリと裏で読み取り、正解のli要素の正規表現パターンからクイズの正誤判定を一括して行うように変更することが出来ます。

            
            <!-- ...中略 -->
<script>
    //...中略
    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_) {
                console.log("checkmark" + m_[1]);
                document.getElementById("checkmark" + m_[1]).checked = true;
            };
        });
    });
</script>
        
ということで、SassとJavascriptとの連携に利用できる::beforeか::afterの擬似要素のcontentプロパティは、色々と使い道がありますので、この機会にじっくり理解を深めておきましょう。


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

まとめ

今回はCSSの擬似要素に設定できるcontentプロパティを使って、四択問題の正誤判定のデータを埋め込む方法を解説してみました。

まだ設問が3つですので、この手法がHTML&CSSミニゲームで強力に効いてくるほどではないですが、設問をガンガン増やしたいならその有り難みが出てきます。

その辺に関しては今後のリファクタリング課題を取り上げた記事で解説していく予定です。

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

以下は古くなったソースコード(バージョン0.2)のバックアップです。参考までに載せておきます。

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>5本</li>
                        <li id="correct-option-1">8本</li>
                        <li>10本</li>
                        <li>足はない</li>
                    </ul>
                </label>
            </div>
            <div id="stage---2" class="stage stage--main stage--main--2">
                <label for="stage2">
                    タコは何動物?
                    <ul class="flexlist">
                        <li>地球外生物</li>
                        <li>緩歩動物</li>
                        <li>実は植物</li>
                        <li id="correct-option-2">軟体動物</li>
                    </ul>
                </label>
            </div>
            <div id="stage---3" class="stage stage--main stage--main--3">
                <label for="stage3">
                    タコの水揚げ量が世界一の国?
                    <ul class="flexlist">
                        <li id="correct-option-3">中国</li>
                        <li>カナダ</li>
                        <li>モーリタニア</li>
                        <li>日本</li>
                    </ul>
                </label>
            </div>
            <div id="stage---reset" class="stage stage--end">
                <p class="score-board"><span id="score"></span></p>
                <label for="reset">もう一度トライ</label>
            </div>
        </form>
        <script>
            document.getElementById("start").addEventListener("change", (e) => {
                document.getElementById("gamestate").textContent = '成績: ';
            });
            document.getElementById("correct-option-1").addEventListener("click", (e) => {
                document.getElementById("checkmark1").checked = true;
            });
            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("correct-option-2").addEventListener("click", (e) => {
                document.getElementById("checkmark2").checked = true;
            });
            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("correct-option-3").addEventListener("click", (e) => {
                document.getElementById("checkmark3").checked = true;
            });
            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択クイズを始めましょう';
            });
        </script>
    </body>
</html>
        

style.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;
                }
            }
        }
    }
    #stage {
        &---op {display: flex}
        @each $item in (1, 2, 3, reset) {
            &---#{$item} { display: none }
        }
    }
    $stages: (
        'start' : 'op',
        'stage1': '1',
        'stage2': '2',
        'stage3': '3'
    );
    @for $i from 1 through length($stages) {
        ##{nth(nth($stages, $i),1)}:checked ~ {
            #stage---#{nth(nth($stages, $i),2)} {display: none}
            @if $i == length($stages) {
                #stage---reset {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                }
            } @else {
                #stage---#{nth(nth($stages, $i+1),2)} {
                    display: flex;
                    opacity: 1;
                    animation: fadeIn 0.3s ease-in 0s forwards;
                    label::before {
                        content: '問題#{$i}:';
                    }
                }
            }
        }
    }
    @keyframes fadeIn {
        0% {
            display: none;
            opacity: 0;
        }
        1% {
            display: flex;
            opacity: 0;
        }
        100% {
            display: flex;
            opacity: 1;
        }
    }
}