3択クイズの選択肢を呼び出し順番をシャッフルさせるための関数の学習です。
これをもとにした3択クイズ制作の学習はこちらです。 https://qiita.com/amanomunt/items/db858308a2cf99e2d3ca
ゲーム概要
選択肢は配列内に{質問文,選択肢[A, B, C]}の形で用意。
回答の値は文字列の配列で、その際先頭のインデックス0番が正解とする。
初期設定
まず表示させるhtmlは以下のようにします。
<p id="question"></p>
<ul id="choices"></ul>
<ul>
の中に選択肢を<li>
として書き加えます。
JavaScript側は以下のように初期設定。
qが質問文で、cが選択肢ですね。c[0]が正解になります。
const quizSet = [
{q: 'What is A?', c: ['A0', 'A1', 'A2']},
{q: 'What is B?', c: ['B0', 'B1', 'B2']},
{q: 'What is C?', c: ['C0', 'C1', 'C2']},
];
let currentNum = 0;
続いてhtmlに書き込むコードを書きます。
※各要素は取得済みとします。
ここでforEachを使うのでおさらいです。
配列.forEach(引数 {処理});
下記の場合、変数choiceにquizSet[currentNum].cをひとつずつ入れて処理をしていきます。
question.textContent = quizSet[currentNum].q; // 質問文の表示
quizSet[currentNum].c.forEach(choice => { // 変数choiceにquizSet[currentNum].cをひとつずつ入れて処理
const li = document.createElement('li'); // li要素を作成
li.textContent = choice; // li要素にchoiceを入れる
choices.appendChild(li); // ul直下に生成
});
これでまず基本の表示はできました。次は選択肢のシャッフルです。
フィッシャー・イェーツのシャッフル
色んなシャッフルのやり方があると思いますが、今回勉強するのは、範囲を狭めながら、最後の要素とランダムに選んだ要素を入れ替えていくフィッシャー・イェーツのシャッフルというアルゴリズムです。
簡単に説明します。
1, まず配列を用意。
[1, 2, 3, 4, 5]
2, その中の最後とランダムに要素を選ぶ。
[1, 2, ③, 4, ⑤]
3, そこを入れ替える。
[1, 2, ⑤, 4, ③]
4, 完了したら、範囲を狭める。
[1, 2, 5, 4] 3
5, その中の最後とランダムに要素を選ぶ。
[1, ②, 5, ④] 3
6, そこを入れ替える
[1, ④, 5, ②] 3
7, 完了したら、範囲を狭める。
[1, 4, 5] 2, 3
8, その中の最後とランダムに要素を選ぶ。 (たまたまランダムと最後の要素がかぶった)
[1, 4, ⑤] 2, 3
9, そこを入れ替える (要素がかぶったためそのまま)
[1, 4, ⑤] 2, 3
10, 完了したら、範囲を狭める。
[1, 4] 5, 2, 3
11, その中の最後とランダムに要素を選ぶ。
[①, ④] 5, 2, 3
12, そこを入れ替える (要素がかぶったためそのまま)
[④, ①] 5, 2, 3
13, 完了したら、範囲を狭める。
[4] 1, 5, 2, 3
14, 終了
4, 1, 5, 2, 3
以上がフィッシャー・イェーツのシャッフルのアルゴリズムになります。このアルゴリズムは偏りがなく、かつ高速にシャッフルすることができるアルゴリズムとして知られています。
これをJavaScriptで書いていきましょう。
シャッフル処理のアルゴリズム
shuffle関数を作ります。引数arrに配列を渡したら、それをシャッフルして返すというものです。
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) { // i = ランダムに選ぶ終点のインデックス
const j = Math.floor(Math.random() * (i + 1)); // j = 範囲内から選ぶランダム変数
[arr[j], arr[i]] = [arr[i], arr[j]]; // 分割代入 i と j を入れ替える
}
return arr;
};
これでひとまずシャッフルさせる関数は書けました。
選択肢を実際にシャッフルしてみる
ではシャッフル関数を使って、選択肢をシャッフルしたものを表示してみましょう。
まずはshuffledChoicesを用意して、そこにシャッフル対象を代入します。
少し前に書いたforEachはそれを使うので、少し書き換えて以下のようにします。
const shuffledChoices = shuffle(quizSet[currentNum].c);
shuffledChoices.forEach(choice => { // 新たに用意した shuffledChoices を使う
const li = document.createElement('li');
li.textContent = choice;
choices.appendChild(li);
});
ひとまず以上になります。
これでhtml上では選択肢がランダムに並び替えられているのですが、ひとつ問題があります。
それは選択肢をシャッフルしたことにより、元の選択肢配列までシャッフルされてしまっていることです。
参照渡しとスプレッド演算子
問題点は以下の箇所にあります。
const shuffledChoices = shuffle(quizSet[currentNum].c);
quizSet[currentNum].cのように配列やオブジェクトがコピーされると値そのものではなく、参照だけがコピーされてしまうからです。。
上記では shuffle関数に渡す引数に quizSet[currentNum].cという配列を渡しています。その渡した配列は['A0', 'A1', 'A2']ですが、この値そのものを渡してるのではなく、この場所の在り処を渡してるためです。従って、shuffle関数内でシャッフルされると大元の配列も変わってしまっています。これを参照渡しといいます。
元々、配列のインデックス0番は正解の選択肢なので、これでは今後の正誤判定に問題が発生するため、スプレッド演算子を使って値のコピーを渡してあげるようにしましょう。
スプレッド演算子(ピリオド3つに配列 / オブジェクト)
...foo
これを使うと下記のようになります。
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
...quizSet[currentNum].cで、配列のコピーが展開されているので、それを[]で囲って新たに配列化させる。
これをシャッフルに使うことで、元の配列には影響なくシャッフルした配列を用意できるようになりました。
まとめ
シャッフルを作るアルゴリズムと、特に参照渡しのところがわかりにくかったのでアウトプットを兼ねて書いてみました。
正直まだ参照渡しのところははっきりと理解できたわけではないので、今後も練習に使えそうな課題を見つけたら取り組んでみたいと思います。
次回からは、これを元にしたクイズゲームを作る学習になります。
以下、今回のコードです。
'use strict';
{
const question = document.getElementById('question');
const btn = document.getElementById('btn');
const choices = document.getElementById('choices');
const quizSet = [
{q: 'What is A?', c: ['A0', 'A1', 'A2']},
{q: 'What is B?', c: ['B0', 'B1', 'B2']},
{q: 'What is C?', c: ['C0', 'C1', 'C2']},
];
let currentNum = 0;
question.textContent = quizSet[currentNum].q;
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[j], arr[i]] = [arr[i], arr[j]];
}
return arr;
}
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
choices.appendChild(li);
});
}