4.6 クイズゲームを作る

(1/1)
クイズゲームに挑戦する陽奈
AI生成コンテンツ / AI-generated content
4.5 神経衰弱ゲームを作る」では、カードの状態を配列で管理し、2枚のランクが一致するかを判定した。本項で作る「クイズゲーム」(quizGame.html)も、画面に表示する内容とゲームの進行状態を分けて管理する。異なるのは、100件の問題文、5つの選択肢、正解、解説、出題元という文章中心のデータを扱う点である。
クイズでは、プレイヤーが正解を知っているかだけでなく、コンピュータが問題を選び、選択肢を混ぜ、回答を判定し、結果を説明する仕組みが必要になる。本項では、問題バンクの設計、オブジェクトと配列による管理、正解情報を失わないシャッフル、ラジオボタンによる回答、誤答時の解説、得点計算、過去成績のランキングという流れを、プログラム経験のない読者にも分かるように順を追って説明する。

問題データはJSONなどの外部ファイルへ分けず、HTMLのJavaScriptに組み込んだ。これは単なる手抜きではない。今回の仕様は、HTMLファイルを直接開くだけで動作し、HTTPサーバーも外部通信も使わないことを求めている。1ファイルにまとめることで、ブラウザや保存場所が変わっても同じ手順で起動できる。その一方で、問題だけを編集しにくくなるという代償もある。本項では、この判断の理由も紹介する。

目次

クイズゲームの概要

クイズゲームの出題画面
クイズゲームの出題画面
AI生成コンテンツ / AI-generated content
本項のクイズゲームは、本連載の記事を題材にした5択式の学習ゲームである。問題バンクには100件を登録し、ゲームを始めるたびに10件をランダムに選ぶ。1回に表示するのは1問だけであり、カテゴリと難易度を見てから答えを1つ選ぶ。正解なら大きな青い○、不正解なら大きな赤い×を表示する。

不正解のときは、正解だけを示して終わりにしない。「なぜその答えなのか」という解説と、元になった記事へのリンクも表示する。クイズを単なる点取りゲームではなく、間違えた箇所へ戻るための復習装置にするためである。10問が終わると、1問10点として100点満点の得点を計算し、ブラウザ内に保存した過去の成績とともに表示する。

ゲーム・ルール

クイズゲームのルール
段階内容
開始100問を並べ替え、その先頭から10問を選ぶ
出題問題、カテゴリ、難易度、5つの選択肢を1問ずつ表示する
回答選択肢を1つ選んで「解答」を押す。未選択の回答は受け付けない
正解青い○を表示し、正解数を1増やす
不正解赤い×、正解、解説、出題元記事へのリンクを表示する
進行判定後に「次の問題」を押す。最終問題では「成績を見る」に変わる
得点正解数に10を掛け、100点満点で表示する
記録回答日時と得点をlocalStorageへ保存し、得点ランキングを表示する
回答済み問題数と正解数は、画面上部へ常に表示する。プレイヤーは「いま何問目か」「何問正解しているか」を途中で確認できる。スマートフォンでも選びやすいように、ラジオボタンだけでなく選択肢を囲む広い領域全体を押せる。判定後はすべての選択肢を無効にし、同じ問題へ二度回答して得点を増やせないようにする。

問題と解答を作る

問題(と、解答選択肢)を作るのにも Codex が活躍する。一度のゲームで出題するのは10問だが、データとしては100問用意しておき、出題の都度、そこからランダムに10問取り出すようにした。
ここでは、本連載の記事を Codex に読み込ませて問題を作ったが、別のソースでも構わない。ただし、ソースの知的財産権には留意されたい。

ここでは、まず元の記事から1つの学習ポイントを選ぶ。たとえば「localStorageはブラウザへ文字列を保存する機能である」という説明を選んだなら、「ブラウザ内にメモを保存する機能はどれですか?」という短い質問へ置き換える。正解は「localStorage」であり、残り4つには「Canvas」「requestAnimationFrame」「AABB」「Pointer Events」のような、講座に登場する別の用語を配置する。

誤答は、誰が見てもでたらめな語だけを並べるのではなく、同じ分野で見かける語を混ぜる。ただし、解釈によって2つが正解になる選択肢を作ってはいけない。問題文を読んだだけで答えが見えたり、正解だけが極端に長かったりするのも避ける。最後に、正解を根拠づける1~2文の解説と、元の記事の節を示すアンカー付きURLを登録する。
1問分に必要なデータ
項目役割
id問題を区別する一意の番号68
category問題の分野Web Storage
difficulty初級・中級・上級の目安初級
question画面に表示する問題文ブラウザ内にメモを保存する機能はどれですか?
choices正解1つと誤答4つlocalStorage、Canvasなど
correct正解が入っている配列位置0
explanation誤答時に示す説明localStorageは文字列データを永続保存できる
source出題元の記事とアンカーcodex01-03-04.shtm#LocalStorageHistory

要素技術1:問題バンクをオブジェクトの配列で管理する

100問を100個の別々の変数へ入れると、選択や並べ替えのたびに特別な処理が必要になる。そこで、問題1件をオブジェクトにまとめ、そのオブジェクトを QUESTION_BANK という1本の配列へ並べる。図書館が1冊ずつの本を同じ書式の目録で管理するように、どの問題も同じ名前の項目を持たせる。

配列にまとめれば、問題数は QUESTION_BANK.length で数えられ、シャッフルも先頭10問の取り出しも共通の処理で済む。新しい問題を加えるときは、配列の末尾へ1件追加すればよい。プログラムは開始時に問題数が100件かを検査し、足りない場合や増えすぎた場合は、誤った条件でゲームを続けず安全に停止する。

要素技術2:問題作成関数で書式をそろえる

問題データは q() という短い関数を通して作る。このように、同じ形のオブジェクトを繰り返し作る関数をファクトリー関数と呼ぶ。q()には問題番号、XMLファイル名、アンカー、カテゴリ、難易度、問題文、正解、4つの誤答、解説を渡す。関数は正解と誤答を1本のchoices配列へまとめ、XMLの拡張子を .shtm に替えて出題元URLも組み立てる。
const q = (
    id, file, anchor, category, difficulty,
    question, correct, wrong, explanation
) => ({
    id,
    category,
    difficulty,
    question,
    choices: [correct, ...wrong],
    correct: 0,
    explanation,
    source: article(file, anchor),
});
正解は必ずchoicesの先頭へ入るため、作成時の正解位置は0である。この約束により、100問すべてへ正解番号を手作業で書く必要がない。4つの誤答を スプレッド構文 の ...wrong で展開する点も新しい。もっとも、表示時まで先頭を正解にしておくと答えが見えてしまうため、次の段階で正解情報ごと並べ替える。

要素技術3:正解情報を保ったまま選択肢を混ぜる

問題と選択肢の並べ替えには、「4.4 落ちものブロックゲーム」と「4.5 神経衰弱ゲーム」でも使ったフィッシャー–イェーツのシャッフルを再利用する。しかし、選択肢の文字だけを混ぜると、どれが正解だったか分からなくなる。そこで、並べ替える前に各選択肢を「文字」と「正解かどうか」の組に変換する。
const choices = shuffle(item.choices.map((text, index) => ({
    text,
    isCorrect: index === item.correct,
})));
return {
    ...item,
    choices,
    correct: choices.findIndex((choice) => choice.isCorrect),
};
<
正解だった選択肢だけ isCorrect が true になる。この印は選択肢と一緒に動くので、何番目へ移っても失われない。並べ替えが終わったあと、findIndex() でtrueの場所を探し、新しいcorrectへ記録する。これは、引っ越す箱に「正解」という札を付け、移動後に札のある棚番号を調べ直すのと同じである。

ゲーム開始時は、まず100問そのものをシャッフルして先頭10問を slice() で取り出し、次にその10問それぞれの選択肢を prepareQuestion() でシャッフルする。出題順の乱数と選択肢順の乱数を二段階に分けることで、同じ問題が選ばれても答えの位置は毎回変わる。

要素技術4:ラジオボタンで回答を受け付ける

5つの選択肢には、HTMLの radioボタンを使う。同じnameを持つradioボタンは同時に1つしか選べないため、「答えは1つ」という規則をブラウザ自身が守ってくれる。各radioボタンのvalueには0~4の配列位置を文字列で入れ、回答時に Number() で数値へ戻す。
const selected = elements.quizForm.querySelector(
    'input[name="choice"]:checked'
);
if (!selected) {
    showMessage('選択肢を1つ選んでください。');
    return;
}
const chosen = Number(selected.value);
const isCorrect = chosen === item.correct;
フォームのsubmitイベントでは、最初に preventDefault() を呼び出す。通常、フォームを送信するとページが移動または再読み込みされるが、クイズではJavaScriptの判定だけを行いたいからである。`:checked`で選択中のradioボタンを探し、見つからなければ理由を表示して処理を戻す。選択値が整数で0~4の範囲にあることも調べ、壊れた値を判定へ渡さない。

判定後は state.answered をtrueにし、5つのradioボタンをdisabledにする。見た目のボタンを隠すだけでなく、プログラムの状態と入力部品の両方をロックしているため、連打や操作のやり直しによる二重加点を防げる。次の問題へ進むと answered をfalseへ戻し、新しいradioボタンを作り直す。

要素技術5:正誤と解説を表示する

選んだ位置とcorrectが一致したら、正解数を1増やして大きな青い○を表示する。不一致なら赤い×に加え、choices[correct].textから正しい選択肢を取り出し、問題データに保存しておいたexplanationとsourceを表示する。正解時は進行を妨げないよう表示を簡潔にし、誤答時にだけ学び直す情報を増やす設計である。

問題文と選択肢は textContent を使って画面へ入れているため、文字の中にHTMLタグのような記号があっても命令として解釈されない。一方、○、×、固定された見出し、解説、リンクをまとめて表示する部分はinnerHTMLを使っている。現在はプログラム内の固定データだけだが、将来、利用者が問題を入力できる機能を追加するなら、解説などもtextContentで組み立てる必要がある。

要素技術6:得点とランキングを計算する

得点計算は「正解数×10」という単純な式である。単純だからこそ、正解した瞬間に得点へ足し込むのではなく、最後に state.correct * 10 で一度だけ求める。正解数が唯一の元データになり、表示得点との食い違いが起きにくい。回答日時はDateで取得してISO形式の文字列として保存し、画面へ出すときだけ Intl.DateTimeFormat で日本語の年月日時分に直す。

成績は「4.4」「4.5」と同じく localStorage へJSON文字列として保存する。読み込み時には配列であること、各行のscoreが有限の数値であること、atが文字列であることを確認する。得点の高い順にsort()し、同点なら日時文字列を比較して新しい記録を先に並べる。表は最大45vhの領域に収め、件数が多いときはその領域をスクロールして閲覧する。

問題データを外部ファイルにしなかった理由

100問をJSONやCSVへ分ければ、プログラムを触らずに問題だけを編集できる。複数の教科へ差し替えたり、問題数を何千件へ増やしたりする場合には外部ファイルが適している。それでも今回は問題をJavaScriptへ直接組み込んだ。仕様が「JavaScriptを使った1本のプログラム・ファイル」「サーバー技術を使わずブラウザで完結」「インターネットと通信しない」と定めているためである。

HTMLをダブルクリックして開くとURLは file:// から始まる。この状態でJavaScriptのfetch()から隣のJSONを読む操作は、ブラウザのセキュリティ制限により失敗することがある。制限を避ける一般的な方法はHTTPサーバーから配信することだが、それでは今回の前提を破る。scriptタグでJSONを読み込む方法も、ファイルが増え、読み込み順や欠落を管理する必要が生じる。
問題データの置き場所の比較
方式利点欠点
HTMLへ内蔵1ファイルで起動・配布でき、データの入れ忘れがないファイルが大きくなり、問題だけを更新しにくい
JSON・CSV問題の編集、交換、再利用、大量管理がしやすいfile://からの読み込みが制限される場合があり、ファイルも増える
データベース検索、共同編集、大量データの更新に強いサーバーと通信が必要で、今回の条件に合わない
したがって、これは「外部ファイルが悪い」という結論ではない。配布方法と実行環境に合わせた選択である。問題を頻繁に入れ替える教材へ発展させるなら、HTTPサーバーの利用を認めたうえでJSONへ分離する価値がある。今回は100問が完成済みで、1人の利用者がオフラインで遊ぶため、保守性より単独動作の確実さを優先した。

ゲーム全体の流れ

状態と処理の流れ
状態主な処理
開始100問を検査し、10問を選び、各問の選択肢を混ぜる
出題中現在の問題と5つのradioボタンを作り、回答を待つ
未選択理由を表示し、出題中へ戻る
判定済み入力を無効化し、○または×と必要な説明を表示する
次の問題問題番号を1増やし、新しい選択肢を作る
終了得点を計算・保存し、ランキングを表示する
システムエラーゲーム画面を隠し、処理を安全に停止して内容を表示する
進行状態は state オブジェクトへ集めている。questionsは今回使う10問、indexは現在の問題位置、correctは正解数、answeredは現在の問題へ回答済みかどうかを表す。画面の表示だけから状態を推測せず、判定に必要な事実をJavaScript側へ持つため、ボタンの表示が一時的に乱れても二重回答を防げる。

通常のイベント処理はtry-catchで囲み、さらにwindowのerrorとunhandledrejectionを受け取る。想定外の例外が起きたらstopSafely()がゲーム画面と成績画面を隠し、エラー専用画面へ内容を表示する。処理を続けて成績を壊すより、原因を示して再読み込みを求めるほうが安全である。

本項で新しく取り上げる要素技術

配列、フィッシャー–イェーツのシャッフル、localStorage、JSON、Intl.DateTimeFormat、Object.fromEntries、ARIA、未処理エラーの捕捉は、これまでの記事ですでに取り上げた。本項では、これらをクイズへ応用するとともに、次の要素を新しく説明する。
本項で新しく取り上げる要素技術
要素技術用途
問題バンク問題文、選択肢、正解、解説などを同じ形のオブジェクトで管理する
ファクトリー関数q()を通して100件の問題データの書式をそろえる
findIndex()シャッフル後に正解の新しい配列位置を探す
radioボタン5つから1つだけを選択させる
フォームのsubmitクリックとEnterキーの回答を同じ入口で受け取る
preventDefault()回答時のページ再読み込みを止める
:checked現在選択されている回答を取得する
replaceChildren()前問の部品を消し、新しい選択肢や成績行へまとめて置き換える
CSSの:has()選択済みradioボタンを含む選択肢全体の見た目を変える
複数条件のsort()得点を優先し、同点なら日時で順位を決める

新しい要素技術1:問題バンク

問題バンクとは、出題に必要な情報を同じ形式でそろえ、1か所へ集めたデータの集合である。本プログラムでは、QUESTION_BANKという配列が問題バンクにあたる。問題文だけでなく、識別番号、カテゴリ、難易度、5つの選択肢、正解位置、解説、出題元URLまでを1件のオブジェクトへまとめる。これにより、「問題文の配列」「解説の配列」のような複数の配列を同じ番号で対応させる必要がない。

もし問題文と解説を別々の配列で管理すると、途中の1件を片方だけ削除したときに番号がずれ、別の問題の解説が表示される危険がある。1問分を1つのオブジェクトにすれば、追加、削除、並べ替えをしても関係する情報が一緒に動く。まとまりとして扱いたいデータは、まとまりのまま保存するのが問題バンクの基本である。
const QUESTION_BANK = [
  {
    id: 1,
    category: 'Codex',
    difficulty: '初級',
    question: 'Codexは何を行うAIですか?',
    choices: ['プログラムを作成・修正する', '動画だけを編集する',
    'OSを更新する', '画像だけを保存する', '印刷だけを行う'],
    correct: 0,
    explanation: 'Codexは自然言語の指示からコードを作成・修正します。',
    source: 'https://www.pahoo.org/...#WhatIsCodex',
  },
];
オブジェクトの項目名が決まっているので、画面を作る処理は「現在の問題のquestionを見出しへ入れる」「choicesをradioボタンにする」のように、どの問題にも同じ命令を適用できる。問題数が100件であることも QUESTION_BANK.length で検査できる。将来、制限時間や出題回数を追加する場合も、各問題へ同じ項目を足すことで拡張できる。

新しい要素技術2:ファクトリー関数

ファクトリー関数は、決まった形のオブジェクトを作って返す関数である。工場が同じ設計図から製品を作ることにたとえた呼び名であり、本プログラムでは q() が担当する。100問すべてに `{ id: ..., category: ... }` と項目名を繰り返し書く代わりに、必要な値だけを決められた順でq()へ渡す。

q()は、正解1件と誤答4件をchoices配列へまとめ、正解位置を0にし、XMLファイル名を公開用の.shtmへ変換してアンカー付きURLを作る。URLの組み立てや初期値の決定を1か所へ集めるため、100問の記述が短くなるだけでなく、全問が同じ規則で作られる。出題元URLの形式を変える場合もq()だけを直せばよい。
const q = (
    id, file, anchor, category, difficulty,
    question, correct, wrong, explanation
) => ({
    id,
    category,
    difficulty,
    question,
    choices: [correct, ...wrong],
    correct: 0,
    explanation,
    source: article(file, anchor),
});
一方、引数を渡す順番を間違えると、カテゴリの場所へ難易度が入るなどの誤りが起きる。q()を使えば内容まで自動的に正しくなるわけではない。作成後には、idの重複、選択肢が5件あること、正解が1件であること、URLのアンカーが存在することを検査する必要がある。関数は書式をそろえる道具であり、問題内容の校正は別の作業である。

新しい要素技術3:findIndex()

findIndex()は、配列を先頭から調べ、条件に最初に一致した要素の位置を返す。本プログラムでは、シャッフルされたchoicesから isCorrect がtrueの選択肢を探す。配列位置は0から始まるため、先頭なら0、2番目なら1、5番目なら4が返る。該当する要素がなければ-1になる。
const correctIndex = choices.findIndex(
    (choice) => choice.isCorrect
);
findIndex()へ渡している `(choice) => choice.isCorrect` は、各要素を受け取り、探している条件に合えばtrueを返す関数である。findIndex()はtrueを見つけた時点で調査を終える。find()が要素そのものを返すのに対し、findIndex()は要素が置かれている番号を返す。今回は回答のvalueも配列番号なので、位置を返すfindIndex()が適している。

正解が見つからず-1になった場合、どの回答を選んでも正解にならない。現在のプログラムはq()が必ず1件へ正解印を付けることを前提にしているが、問題データを利用者が編集できるようにするなら、開始前に結果が-1でないことも検査すべきである。便利な検索命令にも「見つからなかった場合」があることを忘れてはならない。

新しい要素技術4:ラジオボタン

ラジオボタンは、複数の候補から1つだけを選ぶためのHTML入力部品である。input要素のtypeをradioにし、同じ問題の5つへ同じnameを付ける。ブラウザは同じnameのグループ内で1つしか選択状態にしない。プレイヤーが別の候補を選ぶと、前の候補は自動的に解除される。
<label class="choice">
    <input type="radio" name="choice" value="0">
    <span>選択肢の文章</span>
</label>
inputをlabelの内側へ入れると、小さな丸印だけでなく、選択肢の文章や囲み全体をクリックして選べる。指で操作するスマートフォンでは、とくに重要である。valueには表示中の選択肢の配列位置を入れる。valueは文字列として読み出されるので、正解位置と厳密比較する前にNumber()で数値へ変換する。

チェックボックスは複数を同時に選べるため、「正解は1つ」という今回の規則には合わない。select要素でも1つを選べるが、候補を開かなければ全体を見比べられない。5件程度の短い選択肢なら、候補を常に一覧できるラジオボタンがクイズに向いている。

新しい要素技術5:フォームのsubmit

5つのradioボタンと「解答」ボタンはform要素にまとめ、formの submitイベントで回答を受け取る。ボタンのclickだけを監視する方法もあるが、submitならボタンのクリック、画面タッチ、キーボードのEnterキーを同じ処理へ集められる。入力方法ごとに別の判定処理を書く必要がない。
elements.quizForm.addEventListener('submit', (event) => {
    try {
        answer(event);
    } catch (error) {
        stopSafely(error);
    }
});
submitイベントの処理は、回答を読むだけでなく、例外が起きたときにstopSafely()へ渡す入口にもなっている。イベントの入口を1つに集めると、入力検査、正解判定、エラー処理の経路も1つになる。同じ目的の操作は同じ入口で受けると、マウスでは動くがキーボードでは動かない、といった食い違いを減らせる。

新しい要素技術6:preventDefault()

formは本来、入力内容をWebサーバーへ送信するための要素である。送信するとブラウザは別ページへ移動したり、現在のページを読み直したりする。しかし、このゲームはサーバーを使わず、JavaScriptだけで判定する。そこでsubmitイベントの先頭で event.preventDefault() を呼び、ブラウザが本来行う送信動作を取り消す。
const answer = (event) => {
    event.preventDefault();
    // ここからJavaScriptによる入力検査と正解判定
};
preventDefault()はイベントそのものを消す命令ではない。submitイベントを受け取った事実は残り、answer()の残りの処理も続く。止めるのは、イベントに対してブラウザが標準で行う動作だけである。リンクの移動、ファイルのドロップ、右クリックメニューなど、ほかのイベントの標準動作を止める場合にも使われる。

この呼び出しを忘れると、選択肢を選んで「解答」を押した瞬間にページが再読み込みされ、問題の順番や正解数が初期化される可能性がある。見た目は「解答ボタンを押すと最初へ戻る」という不具合になる。フォームをJavaScriptだけで処理するときは、標準の送信を使うのか止めるのかを最初に決める必要がある。

新しい要素技術7::checked

:checkedは、radioボタンやチェックボックスのうち、現在選択されている要素に一致するCSSの疑似クラスである。JavaScriptのquerySelector()でもCSSと同じ選択式を使えるため、`input[name="choice"]:checked` と書けば、choiceグループで選択中のinputを1つ取得できる。
const selected = elements.quizForm.querySelector(
    'input[name="choice"]:checked'
);
if (!selected) {
    showMessage('選択肢を1つ選んでください。');
    return;
}
何も選ばれていないとquerySelector()はnullを返す。そのままselected.valueを読もうとするとエラーになるため、先にnullでないことを確認する。本プログラムは未選択をシステムエラーにせず、「選択肢を1つ選んでください」という利用者が直せる入力エラーとして扱い、同じ問題で回答を待ち続ける。

:checkedは見た目の指定にも使えるが、本プログラムではさらに:has()と組み合わせ、選択されたinputを含むlabel全体へ色を付ける。1つの状態を、JavaScriptでは値の取得に、CSSでは表示の変更に利用している。状態を二重に記録しないため、画面の強調と実際の回答がずれにくい。

新しい要素技術8:replaceChildren()

replaceChildren()は、あるHTML要素の子要素をすべて、新しく渡した要素へ置き換える命令である。引数なしで呼べば中身を空にできる。本プログラムでは、前の問題の5択を消して新しい5択へ入れ替える処理と、成績表の行をまとめて入れ替える処理に使う。
elements.choices.replaceChildren(
  ...item.choices.map((choice, index) => {
    const label = document.createElement('label');
    // radioボタンと文章をlabelへ追加する
    return label;
  })
);
innerHTMLへ長いHTML文字列を代入する方法でも置き換えられるが、引用符の組み合わせを間違えやすく、外部入力が混ざると意図しないHTMLを実行させる危険も生じる。replaceChildren()では、document.createElement()で作った要素を渡すため、画面部品を文字列ではなくオブジェクトとして組み立てることができる。

ここでの `...` はスプレッド構文である。map()が作った5件の配列を、replaceChildren()へ5個の引数として展開する。置き換えは1回で済むため、「古い選択肢を消す」「新しい選択肢を1件ずつ追加する」という処理を別々に管理する必要がない。表示更新の区切りも明確になる。

新しい要素技術9:CSSの:has()

CSSの :has() は、指定した子孫要素を持つ要素を選ぶ疑似クラスである。本プログラムの `.choice:has(input:checked)` は、「選択済みのinputを内側に持つchoice」を表す。radioボタンが選択されると、その外側のlabelの枠を青くし、背景も薄い青へ変える。
.choice:has(input:checked) {
    border-color: var(--blue);
    background: #eaf5ff;
}
従来のCSSは親から子を選ぶのは得意だが、「この子を持つ親」を選ぶのが難しかった。そのためJavaScriptでlabelへselectedクラスを付けたり外したりする必要があった。:has()ならinputのchecked状態をブラウザが監視し、条件に合うlabelへ自動的にスタイルを適用する。状態はHTML、見せ方はCSSという分担を保てる。

:has()は子孫を広く調べられるため、複雑な指定を大量に使うと、どの条件で見た目が変わったのか分かりにくくなる。本例のように「選択された入力部品を含む選択肢」という短く明確な関係へ使うとよい。対応していない古いブラウザでは強調表示が行われないが、radioボタン自体の選択と正解判定は動作する。

新しい要素技術10:複数条件のsort()

sort()は配列の並び順を変更する。本プログラムのランキングには「得点の高い順」という第1条件と、「同点なら新しい回答を先」という第2条件がある。比較関数は、前の要素を先へ置くなら負、後ろへ置くなら正、同じ扱いなら0を返す。
scores.sort(
    (a, b) => b.score - a.score | b.at.localeCompare(a.at)
);
最初の `b.score - a.score` は得点の降順である。bが80点、aが60点なら正の値になり、bが前へ移る。得点が同じなら差は0になる。JavaScriptの `|` は左側が0のとき右側を評価するため、その場合だけ `b.at.localeCompare(a.at)` が使われ、ISO形式の日時文字列を新しい順に並べる。

この書き方は短いが、得点の差が0でなければ日時を比較しないという規則を理解する必要がある。条件が3つ、4つと増える場合は、if文で比較の順番を明示したほうが読みやすい。順位規則はプログラムの都合で決めず、何を優先するランキングなのかを仕様書へ書いてから比較関数へ変換する。

仕様と実装を照合する

添付されたquizGame.htmlは、100問、毎回10問、5択、カテゴリと難易度、○×表示、誤答時の解説と出題元、1問10点、ランキング、スマートフォン対応、エラー表示という中心要件を満たしている。問題データが1ファイルへ内蔵され、外部ライブラリも自動通信もない点も仕様どおりである。

一方、「過去30回分の得点ランキング」という言葉には2つの解釈がある。実装は、保存済み成績へ今回分を加え、全件を得点順に並べてから上位30件を残す。したがって「成績上位30件」である。「直近30回を保存し、その30件に順位を付ける」という意味なら、日時順で古い記録を捨ててから得点順に表示する必要がある。仕様書では、どちらなのかを明示するとよい。

また、仕様の「1画面に収まらない場合は自動スクロールする」に対して、実装はランキング領域へスクロールバーを付け、利用者が操作して閲覧する方式である。勝手に表を動かす自動スクロールは読みづらく、操作しにくいため、この実装のほうが実用的である。ただし、仕様と異なる判断をしたなら、テスト結果で理由を報告して合格判定を受ける必要がある。

クイズゲームのプログラム仕様書

以下が、quizGame.htmlを作るために Codex へ渡したプロンプトである。
「# 問題と解答」は、適当な問題ソースに置き換えてほしい
プログラム仕様書(プロンプト)
# クイズゲーム

# 目標 コンピュータが出題する問題の解答を選択肢から選び、正解の数を競う。
# プログラム・ファイル名 quizGame.html
# プロジェクト・フォルダ作成 - プログラム・ファイル名の拡張子を除いた主ファイル名と同じ名前のサブフォルダを作成し、以降の作業はサブフォルダで行う。 - すでにサブフォルダがあれば、そのサブフォルダに移動して以降の作業を進める。
# 問題と解答 - `codex01` に作成済みの記事から問題と解答選択肢(5つ)を作成する。 - 問題のカテゴリ、難易度、解説、出題箇所(ファイルが `codex01-01-01.xml` であれば、対応するURL `https://www.pahoo.org/e-soul/webtech/codex01/codex01-01-01.shtm` とする;記事中のアンカーがあれば、アンカーを含めて記載する) - 合計100件の問題と回答を作成する。 - 問題と解答は外部ファイルにせず、プログラムに組み込む。
# ゲーム・ルール ## 出題 - コンピュータは、問題の中からランダムに10題を選んで出題する。 - 1回に1つの問題と解答(選択肢)を画面に表示する。 - その問題のカテゴリと難易度を表示する。 - 画面の右上に、回答済み問題数と正解問題数を表示する。
## 解答 - ユーザーは、正解と思う選択肢を1つ選び、「解答」ボタンをクリックする。 - コンピュータは正誤判定を行い、正解の場合は大きな青い○印を、誤答の場合は大きな赤い×印を画面に表示する。 - 誤答の場合は、開設と出題箇所(URLの場合はハイパーリンク)を表示する。
## 成績 - すべての問題が追えたら、1問=10点として、得点を画面に表示する。 - 今回の得点を含め、過去30回分の得点ランキング(回答年月日時分と得点)を表示する。1画面に収まらない場合は自動スクロールする。
## タイトル 常に、画面左上にタイトル「クイズゲーム」とバージョン番号、著作権を表示する。
# エラー処理 - JavaScript実行中にシステムエラーが発生した場合は、処理を安全に停止する。 - エラー内容を画面に表示する - 無効な入力は受け付けず、理由をメッセージで表示する
## 記録 - それまでの成績(上述の通り)
# テスト観点・合格条件 - Codexが5回プレイし、前提条件、制約条件が守られていること。
# 前提条件 - 仕様で分からないことがあれば、ユーザーに質問すること。 - JavaScriptを使った1本のプログラム・ファイルにすること。 - クライアントPCのブラウザ(OSやブラウザの種類は問わない)で動作すること。 - スマホでも利用できること。 - httpサーバなどやNode.jsなどサーバ技術は使わず、ブラウザの機能で完結すること。 - コーディングは「Airbnb JavaScript Style Guide」にのっとること。 - プログラムファイルにコメントとして次の情報を記載すること。 -- プログラムの名称 -- バージョン -- 目的 -- 動作環境 -- 著作権表示および使用条件 -- インストール方法 -- お問い合わせ
# 制約条件 - インターネットとのデータ送受信は行わないこと。 - 外部ライブラリを使用する場合は、下記のサイトに限定すること https://cdn.jsdelivr.net/ https://cdnjs.cloudflare.com/ https://ajax.googleapis.com/ https://code.jquery.com/ https://ajax.aspnetcdn.com/ - プログラムがMIT Licenseに違反していないこと。
# 合格判定 - テスト結果を表示し、合格かどうかをユーザーに質問する。 - 質問が正しければ、以降の処理を進める。
# 簡易取扱説明書の作成 1)HTMLファイルと同じ場所に、簡易取扱説明書のテキストファイルを作成する。ファイル名は "README.txt" にする。 2)説明書には以下の項目を含める。各々の項目は "# 項目名" と表記する。 - プログラムの名称 - バージョン - 目的 - 動作環境 - 著作権表示および使用条件 - インストール方法 - 使い方 - 変更履歴 - お問い合わせ
# 著作権表示および使用条件 このプログラムは OpenAI社の Codex によって作成し、作者が動作を確認しました。 このプログラムは外部とのデータ通信を行いません。
本アプリケーションはMIT Licenseです。 商用を含む無償利用が可能です。自由に改造できます。 再配布の際は、下記の著作権表記、およびURLと本使用条件を必ず明記してください。
Copyright by (c)studio pahoo https://www.pahoo.org/
MITライセンスについては、下記のリンク先を参考にしてください。 http://ja.wikipedia.org/wiki/MIT_License http://www.opensource.org/licenses/mit-license.php
なお,本アプリケーションの利用または改造することによって生じた得失については一切関知いたしません.また,二次利用先の組織・企業・団体の目的・内容・活動については一切関知いたしません.
# お問い合わせ ぱふぅ家のホームページ https://www.pahoo.org/ - サイト案内 - お問い合わせ
# リソース管理 1)今回の作業が、新規作成(評価用)、新規作成(配布用)、メジャーバージョンアップ、マイナーバージョンアップ、不具合修正のいずれに当たるか、ユーザーに質問する。 2)バージョン番号を次のルールで変更し、プログラムファイル、簡易取扱説明書、このプロジェクトのプロンプト(ファイル名は PROMPT.md にする)をGitにコミットする。 -新規作成(評価用)‥‥バージョン0.1.0 -新規作成(配布用)‥‥バージョン1.0.0 -メジャーバージョンアップ‥‥バージョン番号の整数部分を+1 -マイナーバージョンアップ‥‥バージョン番号の小数の1番目を+1 -不具合修正‥‥バージョン番号の小数の2番目を+1
# 配布ファイルの作成 1)プログラムファイル、簡易取扱説明書、このプロンプトを1つのZIPファイルに圧縮する。ZIPファイル名は、"プログラム主ファイル名_バージョン番号.ZIP" の形式にする。 2)完了後、作成したZIPファイルの保存場所を教える。
実際の開発用プロンプトには、READMEの見出し、MIT Licenseの使用条件、バージョン番号の決め方、Gitコミット、ZIPファイル名も詳しく記載した。機能だけでなく、テスト、合格判定、説明書、配布まで書くことで、Codexが「画面が動いた」時点で作業を終えてしまうのを防いでいる。

参考サイト

(この項おわり)
header