【Html&Cssで作る四択クイズゲーム開発記録⑦】Svelteアプリの作り方入門!HTML&CSSゲームをSvelteプロジェクトへ移行(マイグレーション)する


※ 当ページには【広告/PR】を含む場合があります。
2022/01/18
【Html&Cssで作る四択クイズゲーム開発記録⑥】ネイティブなJavascriptスクリプトをフル活用してHTMLタグ要素(DOM)を自動生成する
合同会社タコスキングダム|TacosKingdom,LLC.

JSフレームワークの中でもビルド後の生成ファイルのサイズが最軽量級で、コードもスッキリしていて、初学者に今もっともオススメの
『Svelte』でHTML&CSSミニゲームを作るための手順を考えていきます。

最近、別のブログの方で、Svelteアプリの開発環境をサクッと構築するやり方を解説してみました。

参考|【Svelte入門】Svelteの開発環境をDocker Alpine内で簡単に構築する

Svelteを始めてみたい方はそちらの記事をじっくり読んで頂くとして、この記事の方ではこれまでピュアなHTML&CSSで作ってきた四択クイズアプリを
Svelteアプリとして再構築・マイグレーションする手順を解説していきます。


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

最軽量なコードが書けるJSフレームワーク・「Svelte」

そもそもフレームワークとは「枠組み」や「骨組み」という意味の単語です。

ソフトウェア開発界隈では、何かの言語でアプリケーションを作り始めたいと思っているソフトウェア開発者が一から全て作り始めるのが大変なので、そのアプリ開発の土台になるベース機能を提供するソフトウェアを指す用語として、「アプリケーションフレームワーク」もしくは単に「フレームワーク」と呼ばれています。

最終的どのようなアプリケーションを作るかによって、適切なフレームワークを選ぶ必要があります。

とは言え全てのフレームワークで共通していえるのは、
「いかに開発者に楽をさせられて、プログラミング工数の削減・圧縮に貢献できるか」という思想のもとで作られています。

フレームワーク選びにおいて良く耳にする、「どのフレームワークが良いですか?」という質問が良くあります。

これに対する答えはそのご本人の中にしか存在せず、「自分はどんな感じに“楽”したいんだろう?」と自問自答する他ありません。

フレームワークを使い始める初級者ユーザーにとって一番悩ましいのが、Javascript/Typescriptの勉強とは別に「フレームワーク特有のラーニングコスト(学習コスト)」が大なり小なり存在します。

フレームワークは導入当初からバリバリ使える訳でもなく、まずはフレームワークを使う上でのインストール方法や、コードを実装する作法を覚える必要があるのです。

初学者が最初にフレームワーク選びを間違うと、楽な作業とは全く無縁の
「勉強のための苦行」に陥りやすく、ただただ苦い経験になってしまいます。

ということで、HTML&CSSミニゲームのような簡単なアプリケーションを作成する上で、この"学習コスト"が最小クラスの「Svelte」が現状でもっとも最適な選択肢と言えるでしょう。

既存のJavascriptコードをonMount関数で動かす

今回の狙いは過去にHTMLアプリとしてネイティブなJavascriptコードで作ったレガシーコードを、いかにSvelteアプリとして再構築するかに主眼をおいてからスタートします。

そこで困るのが、
document.addEventLister('DOMLoaded'...)が早速Svelteでは使えないことです。

DOMを読み込んでHTMLページを返すまでには、Svelte独自の
ライフタイムを理解する必要があります。

DOMLoadedのようにDOMがSvelte側が全てレンダリングした後で処理を始めたい場合、onMountというライフサイクル関数のコールバックとして呼び出すことが可能です。

前回のバージョン0.7のソースコードでは、javascriptスクリプトでcreateElementを拡張した手動DOM挿入関数を自作する方法をとったのですが、この自作関数をそのままSvelteのonMount関数で利用するのはライフサイクルの仕組み上、混ぜると相性が悪いので、前々回までの修正コード(バージョン0.6)まで引き戻してから静的DOMを操作するようにSvelteコードに仕立て直します。

            
            <script lang="ts">
    import { onMount } from 'svelte';
    onMount(() => {
        document.getElementById("start").addEventListener("change", (e) => {
            document.getElementById("gamestate").textContent = '成績: ';
        });
        function checkGameOver() {
            let isGameOver = true;
            document.querySelectorAll("input[id^='stage']").forEach((checkItem: any) => {
                isGameOver = isGameOver && checkItem.checked;
            });
            return isGameOver;
        }
        function checkSum() {
            let result = 0, total = 0;
            document.querySelectorAll("input[id^='checkmark']").forEach((checkItem: any) => {
                checkItem.checked && result++;
                total++;
            });
            document.getElementById("score").textContent = result == total ? `全問正解🎉あなたは「タコマスター」の称号を得ました!!` : `正解率は ${result}/${total} でした。`;
        }
        [...Array(6).keys()].map(m => {
            return {stageName: `stage${m+1}`, checkbox: `checkmark${m+1}`};
        }).forEach((g: any) => {
            document.getElementById(g.stageName).addEventListener("change", (e) => {
                document.getElementById("gamestate").textContent += (document.getElementById(g.checkbox) as any).checked ? '⭕ ' : '❌ ';
                checkGameOver() && checkSum();
            });
        });
        document.getElementById("reset").addEventListener("click", (e) => {
            document.getElementById("gamestate").textContent = 'さあ4択クイズを始めましょう';
        });
        document.querySelectorAll("ul.flexlist > li").forEach((userItem) => {
            userItem.addEventListener("click", (e: any) => {
                const m_ = window.getComputedStyle(e.target,'::after').content.match(/(\d+)\/correct/);
                m_ && ((document.getElementById(`checkmark${m_[1]}`) as any).checked = true);
            });
        });
    });
</script>

<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="stage4"/>
    <input type="checkbox" id="stage5"/>
    <input type="checkbox" id="stage6"/>
    <input type="checkbox" id="checkmark1"/>
    <input type="checkbox" id="checkmark2"/>
    <input type="checkbox" id="checkmark3"/>
    <input type="checkbox" id="checkmark4"/>
    <input type="checkbox" id="checkmark5"/>
    <input type="checkbox" id="checkmark6"/>
    <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><li></li><li></li></ul>
    </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>
    </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>
    </div>
    <div id="stage---4" class="stage stage--main stage--main--4">
        <label for="stage4">
        <ul class="flexlist"><li></li><li></li><li></li><li></li></ul>
    </div>
    <div id="stage---5" class="stage stage--main stage--main--5">
        <label for="stage5">
        <ul class="flexlist"><li></li><li></li><li></li><li></li></ul>
    </div>
    <div id="stage---6" class="stage stage--main stage--main--6">
        <label for="stage6">
        <ul class="flexlist"><li></li><li></li><li></li><li></li></ul>
    </div>
    <div id="stage---reset" class="stage stage--end">
        <p class="score-board"><span id="score"></span></p>
        <label for="reset">もう一度トライ</label>
    </div>
</form>

<style lang="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%;
    }
    $stages: (
        'start' : ('op', ''),
        'stage1': ('1', 'タコの足は何本?', ('5本', '8本', '10本', '足はない'), 2, blue),
        'stage2': ('2', 'タコは何動物?', ('地球外生物', '緩歩動物', '実は植物', '軟体動物'), 4, rgb(231, 185, 99)),
        'stage3': ('3', 'タコの水揚げ量が世界一の国?', ('中国','カナダ','モーリタニア','日本'), 1, rgb(211, 51, 203)),
        'stage4': ('4', 'タコの心臓は何個?', ('1個','2個','3個','8個'), 3, rgb(68, 67, 8)),
        'stage5': ('5', 'タコの弱点は?', ('イソギンチャク','直射日光','真水','ウミウシ'), 3, rgb(179, 53, 15)),
        'stage6': ('6', 'タコの脳は何個?', ('3個','9個','12個','実は存在しない'), 2, rgb(79, 100, 4))
    );
    .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;
            }
            @for $i from 2 through length($stages) {
                $item: nth($stages,$i);
                $item: nth($item,2);
                &--#{nth($item,1)} {
                    color: nth($item,5);
                    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 }
        @for $i from 2 through length($stages) {
            $item: nth($stages,$i);
            $item: nth($item,2);
            &---#{nth($item,1)} { display: none; }
        }
        &---reset { display: none; }
    }
    @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;
        }
    }
}
</style>
        
これをそのままビルドしても動作は変わらないことが確認できます。

以下はこのSvelteコードをビルド後のアプリです。

これで
超お手軽にSvelteアプリに移植完了!と行きたいところですが、これでは単にonMount関数の助けを借りてネイティブのJavascriptコードを呼び出しているだけです。

できるだけonMountの助けの要らない/必要としないようにコーディングしないと、真のSvelteアプリとは言えません。

以降では、とりあえず全ての関数をonMountの外に出すようにSvelteスクリプト側を改造していきます。


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

Svelteへ移行するときの勘所

Svelteの全てのテクニックをじっくり解説していくと、本記事1ページどころでは済まないですので、プログラミングが分からなくなったら公式のドキュメントから勉強するように心がけましょう。

ここではJavascriptのスクリプトコードからSvelteへのマイグレーションの際に良く使うであろうポイントを掻い摘む感じでまとめます。

document.getElementByIdの置き換え

document.getElementByIdは特定のDOM要素を直接操作するときに良く使うテクニックですが、Svelteではbind:thisで代替できます。

            
            <script lang="ts">
    let gameStateSpan: any;
    //...中略
</script>

// ...中略
<p class="game-header">
    //👇変数・gameStateSpanにこのspan要素をバインド
    <span id="gamestate" bind:this="{gameStateSpan}">さあ4択クイズを始めましょう</span>
</p>
// ...以下略
        
このようにbind:thisはHTML要素のパートを、スクリプトパートに持ち込んで処理をするのに便利です。

一つのHTML要素内で排他的に完結する処理を行うのであれば、後述する
on:イベント名を使ってHTML要素に直接スクリプトを記述する方がSvelte的なコーディング方法と言えます。

この
on:イベント = {イベントハンドラ}と要素に直接仕込むスクリプトの記述の方が、bind:thisより読みやすいので、出来るだけbind:thisを少なくしたほうがコードの可読性が上がります。

document.querySelectorAllの書き換え

基本的にはbind:thisを使って、直接操作したい要素を1対1に対応する変数を用意して、スクリプトパートをリアクティブに扱うのがSvelte流です。

もっぱらInput要素のグループ化に用いられる
bind:groupと言う機能があります。こちらは名前が紛らわしいですが、Input要素以外の任意の要素のDOM要素には柔軟に使えないので、取り扱いには注意が必要です。

もしも
document.querySelectorAllの置き換えが必要であるなら、bind:thisを使ってグループ化を模した配列を使うのも手です。

例えば、

            
            <script lang="ts">
    //...中略
    //👇グループ化したい要素を入れる空の配列を用意しておく
    let stageCheckboxGroup = [];
    let stageCheckmarkGroup = [];
    let stageQuizUlGroup = [];
    //...中略
</script>

// ...中略
{#each stages as stage, i}
    //👇グループ化先の配列を指定
    <input type="checkbox" id="stage{stage.id}" bind:this="{stageCheckboxGroup[i]}"/>
    //👇グループ化先の配列を指定
    <input type="checkbox" id="checkmark{stage.id}" bind:this="{stageCheckmarkGroup[i]}"/>
{/each}

{#each stages as stage, i}
    <div id="stage---{stage.id}" class="stage stage--main stage--main--{stage.id}">
        <label for="stage{stage.id}">
        //👇グループ化先の配列を指定
        <ul class="flexlist" bind:this="{stageQuizUlGroup[i]}">
            <li></li><li></li><li></li><li></li>
        </ul>
    </div>
{/each}
// ...以下略
        
と言うように#eachループ構文でも使いやすいと思います。

clickイベントの書き換え

要素に仕込みたいイベントは基本的にon:イベント名で要素タグを直接装飾するのがSvelte流です。

要素タグとイベント名とその発火する関数の定義がとても近い位置にあり、簡潔にコードが書けて、しかも何が起こっている・起こるのかがコーディングする人間に分かりやすく書けるのがSvelteの良いところです。

もっとも良く使うのが
on:clickだと思います。

            
            <script>
    //👇ゲーム処理で利用する変数(一例)
    let gameStateSpan: any;
    let result = 0;
    let total = 0;
    $: clearStageArr = Array(6).fill(false);
    $: checkmarkArr = Array(6).fill(false);

    //...省略

    //👇スクリプト内の変数の初期化
    function initGame() {
        gameStateSpan.textContent = 'さあ4択クイズを始めましょう';
        checkmarkArr = Array(6).fill(false);
        clearStageArr = Array(6).fill(false);
        result = 0;
        total = 0;
    }

    //...省略
</script>

//...省略

    <div id="stage---reset" class="stage stage--end">

        //...省略

        //👇form要素のリセット処理と同時にスクリプトの変数の初期化もする
        <label for="reset" on:click="{e => initGame()}">もう一度トライ</label>
    </div>

//...以下略
        
このon:イベント = {イベントハンドラ}というのをHTMLタグへ簡潔に直接記述するのがSvelteの大きなアドバンテージの一つになっています。

このテクニックを上手く扱うことで、他のJSフレームワークより行数の少ないコードでアプリ開発が可能になるでしょう。

changeイベントの書き換え

ネイティブなJavascriptコードだと、changeイベントを仕込むものかなりの手間がありました。

            
            document.getElementById("要素ID").addEventListener("change", (e) => {
    //イベントハンドラの中身...
});
        
これが、Svelteだと特定のHTML要素に直接Changeイベントのハンドラ関数を組み込むことができます。

            
            <input type="checkbox" id="start" on:change="{e => { gameStateSpan.textContent = '成績: ';}}"/>
        
とても直観的で簡潔な記述になっています。

checkboxのチェック判定の書き換え

任意の要素の属性値をバインドするためのbind:プロパティは応用の広い機能です。

かなりこの記事でも使っているテクニックに、checkboxタイプにしたinput要素のチェック状態を判定するやり方を使います。

SvelteのInputチェックボックスの状態を管理するために、スクリプトパートで配列を用意して、そこにバインドさせます。

            
            <script>
    //👇チェック状態を記録しておくための配列(一例)
    $: clearStageArr = Array(6).fill(false);
    $: checkmarkArr = Array(6).fill(false);

    //...省略
</script>

///...HTML要素パート

    {#each stages as stage, i}
        <input type="checkbox" id="stage{stage.id}" bind:checked="{clearStageArr[i]}" />
        <input type="checkbox" id="checkmark{stage.id}" bind:checked="{checkmarkArr[i]}"/>
    {/each}
        
このチェック状態を持たせている配列は、リアクティブ($:)にしているため、きちんとチェックボックスの数を揃えて、falseに初期化しておかないと、チェックボックスの動作に影響を与えてしまうので注意が必要です。

Svelteアプリへ移行後のソースコード(暫定版)

で、先程のonMountで動かしていたコードを以上のポイントからざっと書き換えてみたものが以下になります。

            
            <script lang="ts">
    let stages = [...Array(6).keys()].map(m => ({id: `${m+1}`, stageName: `stage${m+1}`, checkbox: `checkmark${m+1}`}));
    let result = 0, total = 0;
    let gameStateSpan: any;
    $: clearStageArr = Array(6).fill(false);
    $: checkmarkArr = Array(6).fill(false);
    $: resultMessage = result == total ? `全問正解🎉あなたは「タコマスター」の称号を得ました!!` : `正解率は ${result}/${total} でした。`;
    function initGame() {
        gameStateSpan.textContent = 'さあ4択クイズを始めましょう';
        checkmarkArr = Array(6).fill(false);
        clearStageArr = Array(6).fill(false);
        result = 0, total = 0;
    }
    function liClick(event: any) {
        const m_ = window.getComputedStyle(event.target,'::after').content.match(/(\d+)\/correct/);
        if (m_) {
            checkmarkArr[parseInt(`${m_[1]}`, 10) - 1] = true;
            result++;
            gameStateSpan.textContent += '⭕ ';
        } else {
            gameStateSpan.textContent += '❌ ';
        };
        total++;
    }
    function invalidClick(event: any){
        gameStateSpan.textContent += '😢 ';
        total++;
    }
</script>

<form id="game-wrapper">
    <p class="game-header"><span id="gamestate" bind:this="{gameStateSpan}">さあ4択クイズを始めましょう</span></p>
    <input type="reset" id="reset"/>
    <input type="checkbox" id="start" on:change="{e => { gameStateSpan.textContent = '成績: ';}}"/>

    <div id="stage---op" class="stage stage--op"><label for="start">クリックしてスタート</label></div>

    {#each stages as stage, i}
        <input type="checkbox" id="stage{stage.id}" bind:checked="{clearStageArr[i]}" />
        <input type="checkbox" id="checkmark{stage.id}" bind:checked="{checkmarkArr[i]}"/>
    {/each}

    {#each stages as stage}
        <div id="stage---{stage.id}" class="stage stage--main stage--main--{stage.id}">
            <label for="stage{stage.id}" on:click="{e => invalidClick(e)}">
            <ul class="flexlist">
                {#each Array(4) as _}
                    <li on:click|stopPropagation="{e => liClick(e)}">
                {/each}
            </ul>
            </label>
        </div>
    {/each}

    <div id="stage---reset" class="stage stage--end">
        <p class="score-board"><span id="score" contenteditable="true" bind:textContent={resultMessage}></span></p>
        <label for="reset" on:click="{e => initGame()}">もう一度トライ</label>
    </div>
</form>

<style lang="scss">
//...中身は先程と同じなので省略
</style>
        
ご覧のように、Svelteの強力な文法略記・圧縮の能力により、何割かコードが効率化できたようです。


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

まとめ

今回から四択クイズもSvelteアプリへ移行し、ゲームの機能としては全く変わらないですが、中身は別物にリニューアルできました。

もしかすると、「Svelteアプリ 作り方」でネット検索されたらこの記事に辿り着かれた方もいらっしゃるかも知れません。

次回以降では、HTML&CSSの標準関数だけでゲーム機能を盛り込んできた経緯もあり、かなり無理矢理にSass側に実装していたデータ類などを、Svelte側へ移していく内容をやっていこうかと思います。

付録〜旧ソースコードのバックアップ

前回までのプロジェクトのソースコードを以下にバックアップしておきます。

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"></form>
        <script>
        (function() {
            const createElement = (tagName, attrs) => {
                const _e = document.createElement(tagName);
                if (attrs) {
                    for (let attr in attrs) { _e[attr] = attrs[attr]; }
                }
                return _e;
            };
            document.addEventListener("DOMContentLoaded", () => {
                const stageLength = 6;
                const parent = document.getElementById('game-wrapper');

                parent.appendChild(createElement('p', {
                    className: 'game-header'
                })).appendChild(createElement('span', {
                    id: 'gamestate',
                    innerHTML: 'さあ4択クイズを始めましょう'
                }));
                parent.appendChild(createElement('input', { id: 'reset', type: 'reset' }));
                parent.appendChild(createElement('input', { id: 'start', type: 'checkbox' }));

                parent.appendChild(createElement('div', {
                    id: 'stage---op',
                    className: 'stage stage--op'
                })).appendChild(createElement('label', {
                    htmlFor: 'start',
                    innerHTML: 'クリックしてスタート'
                }));

                for (let i=1; i<=stageLength; i++) {
                    parent.appendChild(createElement('input', { id: `stage${i}`, type: 'checkbox' }));
                    parent.appendChild(createElement('input', { id: `checkmark${i}`, type: 'checkbox' }));
                    const ul_ = parent.appendChild(createElement('div', {
                        id: `stage---${i}`,
                        className: `stage stage--main stage--main--${i}`
                    })).appendChild(createElement('label', {
                        htmlFor: `stage${i}`
                    })).appendChild(createElement('ul', {
                        className: 'flexlist'
                    }));
                    for (let j=0;j<4;j++) {
                        ul_.appendChild(document.createElement('li'));
                    }
                }

                const childResetDiv = parent.appendChild(createElement('div', {
                    id: 'stage---reset',
                    className: 'stage stage--end'
                }));
                childResetDiv.appendChild(createElement('p', {
                    className: 'score-board'
                })).appendChild(createElement('span', { id: 'score' }));
                childResetDiv.appendChild(createElement('label', {
                    htmlFor: 'reset',
                    innerHTML: 'もう一度トライ'
                }));

                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 ? `全問正解🎉あなたは「タコマスター」の称号を得ました!!` : `正解率は ${result}/${total} でした。`;
                }
                [...Array(stageLength).keys()].map(m => {
                    return {stageName: `stage${m+1}`, checkbox: `checkmark${m+1}`};
                }).forEach(g => {
                    document.getElementById(g.stageName).addEventListener("change", (e) => {
                        document.getElementById("gamestate").textContent += document.getElementById(g.checkbox).checked ? '⭕ ' : '❌ ';
                        checkGameOver() && checkSum();
                    });
                });

                document.getElementById("start").addEventListener("change", (e) => {
                    document.getElementById("gamestate").textContent = '成績: ';
                });
                document.getElementById("reset").addEventListener("click", (e) => {
                    document.getElementById("gamestate").textContent = 'さあ4択クイズを始めましょう';
                });
                document.querySelectorAll("ul.flexlist > li").forEach((userItem) => {
                    userItem.addEventListener("click", (e) => {
                        const m_ = window.getComputedStyle(e.target,'::after').content.match(/(\d+)\/correct/);
                        m_ && (document.getElementById("checkmark" + m_[1]).checked = true);
                    });
                });
            });
        }());
        </script>
    </body>
</html>
        
の内容をindex.htmlの名前で適当なフォルダの中に保存する。

次にhtmlのスタイリングのファイルである
style.cssを先程のindex.htmlと同じフォルダにおいておく必要がある。

以下はstyle.cssの元になる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%;
    }
    $stages: (
        'start' : ('op', ''),
        'stage1': ('1', 'タコの足は何本?', ('5本', '8本', '10本', '足はない'), 2, blue),
        'stage2': ('2', 'タコは何動物?', ('地球外生物', '緩歩動物', '実は植物', '軟体動物'), 4, rgb(231, 185, 99)),
        'stage3': ('3', 'タコの水揚げ量が世界一の国?', ('中国','カナダ','モーリタニア','日本'), 1, rgb(211, 51, 203)),
        'stage4': ('4', 'タコの心臓は何個?', ('1個','2個','3個','8個'), 3, rgb(68, 67, 8)),
        'stage5': ('5', 'タコの弱点は?', ('イソギンチャク','直射日光','真水','ウミウシ'), 3, rgb(179, 53, 15)),
        'stage6': ('6', 'タコの脳は何個?', ('3個','9個','12個','実は存在しない'), 2, rgb(79, 100, 4))
    );
    .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;
            }
            @for $i from 2 through length($stages) {
                $item: nth($stages,$i);
                $item: nth($item,2);
                &--#{nth($item,1)} {
                    color: nth($item,5);
                    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 }
        @for $i from 2 through length($stages) {
            $item: nth($stages,$i);
            $item: nth($item,2);
            &---#{nth($item,1)} { display: none; }
        }
        &---reset { display: none; }
    }
    @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;
        }
    }
}