クイズゲームの作り方:問題データの管理と出題
遊びをつくる執筆にClaudeを使用しています。

クイズゲームを作るとき、最初につまずくのはたいていロジックではなく「問題データをどこに置き、どう呼び出すか」です。10問なら配列に直書きしても動きますが、50問・100問になると修正のたびにコードを開き直すことになり、問題の追加や差し替えが億劫になります。 この記事では、問題データの持ち方・読み込み方・出題順の制御という三つの軸に沿って、実用的な判断基準と手順を整理します。UIデザインやスコア演出よりも「データとロジックの分離」に焦点を当てています。これを先に決めておくと、あとの機能追加がずっとスムーズになります。
問題データをどこに置くか
最も手軽なのは、JavaScriptファイルの中に配列としてそのまま書く方法です。
const questions = [
{ q: "日本の首都は?", choices: ["東京","大阪","京都","名古屋"], answer: 0 },
...
];問題数が20問程度で、自分しか編集しないなら悪い選択ではありません。ただし問題が増えると、コードと内容が混在して読みにくくなります。
問題が30問を超えたら、データを別ファイルに切り出すことを検討します。よく使われる形式はJSONです。questions.json というファイルに配列を置き、ゲーム起動時に fetch() で読み込みます。JSONはテキストエディタで編集しやすく、コードの知識がない共同制作者でも問題の追加・修正ができます。
[
{ "id": 1, "q": "日本の首都は?", "choices": ["東京","大阪","京都","名古屋"], "answer": 0 },
{ "id": 2, "q": "富士山の標高は?", "choices": ["3776m","3675m","3926m","3550m"], "answer": 0 }
]CSVで管理してJavaScriptでパースする方法もあります。スプレッドシートで問題を作って書き出したい場合に向いていますが、選択肢の数が問題ごとに異なるケースでは列設計が複雑になるため、JSONの方が汎用性があります。
選択基準のまとめ:
- ~20問・自分だけが編集、推奨形式:JSファイル内の配列
- 30問以上・追加が続く、推奨形式:外部JSON + fetch
- スプレッドシートで管理したい、推奨形式:CSV → JSONに変換してから使う
JSONを読み込んで出題する基本フロー
fetch() はHTTPリクエストを使うため、ローカルのHTMLファイルをそのままブラウザで開くと(file:// プロトコル)セキュリティ制限でエラーになる場合があります。開発中はローカルサーバー(npx serve や VS Codeの Live Server 拡張など)を使うか、開発段階では配列直書きにしておいて最後にJSON化するという進め方が現実的です。
読み込みの基本構造は次のようになります。
let questions = [];
async function loadQuestions() {
const res = await fetch("questions.json");
questions = await res.json();
startQuiz();
}
loadQuestions();startQuiz() を fetch の完了後に呼ぶことで、データが揃う前に出題しようとするタイミングのずれを防げます。非同期処理のミスはクイズゲームでよくあるバグの原因になるため、「データ取得 → ゲーム開始」という順序を関数の構造で保証しておくことが大切です。
出題順の制御:シャッフルと重複なし抽選
問題をそのまま0番から順に出すと、繰り返しプレイしたユーザーが答えを丸暗記してしまいます。毎回順序をランダムにするのが基本です。
配列のシャッフルにはフィッシャー–イェーツ法がよく使われます。
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
const shuffled = shuffle([...questions]); // 元の配列を変えないようスプレッド元の questions 配列をそのまま渡すと順序が書き換わります。スプレッド構文でコピーしてからシャッフルすると、再スタート時にまた同じデータを使えます。
全問ではなく「全体から10問だけ出す」という場合は、シャッフル後に slice(0, 10) で先頭10件を取り出すのが最もシンプルです。
カテゴリ・難易度で絞り込む
問題データにカテゴリや難易度フィールドを追加しておくと、ユーザーが選んで出題できるようになります。
{ "id": 5, "category": "地理", "level": 2, "q": "...", "choices": [...], "answer": 1 }出題前に filter() で絞ります。
const pool = questions.filter(q => q.category === selectedCategory);
const shuffled = shuffle([...pool]).slice(0, 10);この設計をしておくと、後から「難しいモード」や「ジャンル別クイズ」を追加するときにデータを変えるだけで済みます。
選択肢のシャッフルと正解インデックスの更新
問題自体をシャッフルするだけでなく、各問題の選択肢の並び順もランダムにした方が「常に1番目が正解」という規則性を排除できます。
選択肢をシャッフルすると、正解を示す answer のインデックス番号がずれます。対処法は二つあります。
方法A:正解をインデックスではなく文字列で持つ
{ "q": "日本の首都は?", "choices": ["東京","大阪","京都","名古屋"], "answer": "東京" }選択肢をシャッフルしても answer の値(文字列)は変わらないため、正誤判定に choices[i] === answer が使えます。
方法B:シャッフル後に正解インデックスを再計算する
選択肢をシャッフルしてから、正解の文字列が何番目に来たかを indexOf() で調べてインデックスを更新します。どちらでも動きますが、方法Aの方がコードが単純で、データを手で編集するときもミスが起きにくいです。
問題データ設計でよくある失敗
選択肢の数をコードに決め打ちにする 「選択肢は必ず4個」という前提でループを書くと、3択や5択の問題を追加したとき動かなくなります。choices.length を使って動的に生成すると柔軟に対応できます。
答え合わせを選択肢のインデックスだけで判定する 上で触れた通り、選択肢シャッフル後にインデックスがずれることがあります。文字列比較か再計算のどちらかで対策します。
問題IDを連番にする 問題を途中で削除すると連番が飛びます。IDで状態管理(「この問題は解いた」など)をしているとバグになります。IDは連番でなくても一意であれば十分で、削除・追加が起きてもほかの問題に影響しない設計にします。
フィードバック表示のタイミングを問題データに混ぜる 「この問題を解いたら解説を出す」というロジックをデータ側に書くと、表示制御とデータが分離できなくなります。解説文はデータに持たせて構いませんが、「いつ表示するか」はゲームのロジック側で決めるのが保守しやすいです。
次の行動
問題データをJSONファイルに切り出し、fetch() で読み込む構成を一度試してみてください。まず10問程度のJSONを作り、シャッフルして表示するだけのプロトタイプを動かすと、データ設計の感覚が具体的につかめます。動くものができたら、アソバースにZIPまたはHTMLファイルとして投稿し、ほかのユーザーに実際にプレイしてもらいながら問題を増やしていくサイクルが、完成に向けた最短ルートです。

