前回作ったシャッフル機能を使った三択クイズを作ってみたいと思います。
機能概要
・問題の選択肢の順番をランダムに
・クリックすると正解不正解がわかる
・最後にスコア表示
・リプレイ機能
以上です。
では作っていきます。まずはhtmlを簡単に。大枠の中に質問文エリアと選択肢エリア、次へ進むボタンエリアを用意します。
<div class="container">
<p id="question">What is A?</p> <!--質問文エリア-->
<ul id="choices"></ul> <!--選択肢エリア-->
<div id="btn">Next</div> <!--ボタンエリア-->
</div>
細かいスタイリングは省きまして、肝心なJavaScriptの方を重点的に見ていきます。
まずは各要素の取得と問題を用意し、htmlに表示させます。
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; // currentNum問目の質問文を表示
選択肢エリアは選択仕事に<li>
を作成し、その中にいれていきます。
やりかたは quizSet[currentNum].c
を forEachで回し、その処理の中で<li>
を作りつつ、値を入れていきます。
quizSet[currentNum].c.forEach(choice => { // choiceに quizSetのcurrentNum番目のc要素をひとつずつ渡す
const li = document.createElement('li'); // <li>要素を生成する
li.textContent = choice; // 生成した<li>に choice を代入する
choices.appendChild(li); // <ul id="choices">直下に上記の li を入れる
});
これでひとまず、質問文を質問文エリアに、選択肢を選択肢エリアに埋め込むことができました。
選択肢をシャッフルさせる
前回学習した 【JavaScriptによる要素のシャッフルについての学習】 (https://qiita.com/amanomunt/items/7bd686f58b9cc59d9d07) を使って選択肢をシャッフルさせる関数を用意します。
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);
});
}
これにより、元データにも影響のない、かつ正しくシャッフルされた選択肢が用意できました。
正誤判定
その前に画面描画に関する処理をsetQuiz関数を作ってまとめてしまいましょう。
function setQuiz() { // クイズ描画に関する処理まとめ
question.textContent = quizSet[currentNum].q;
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
choices.appendChild(li);
});
}
setQuiz();
正誤判定は選択肢である<li>
要素をクリックした際に実行します。従って上記の<li>
要素を生成したところにクリックイベントを追加します。
その正誤判定用にcheckAnswer関数を用意して、クリックした時にそれを実行するようにします。
※正解は常にインデックス0番になります。
ちなみに正解だった場合は、<li>
要素にcorrectクラスを、不正解ならwrongクラスを追加するようにします。
function checkAnswer(li) { // 正誤判定関数
if (li.textContent === quizSet[currentNum].c[0]) { // クリックしたli要素のテキストが、c[0] = 正解と同じだったら
li.classList.add('correct'); // 正解処理
} else {
li.classList.add('wrong'); // 不正解処理
}
}
function setQuiz() { // クイズ描画に関する処理まとめ
question.textContent = quizSet[currentNum].q;
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => { // クリックイベントを追加
checkAnswer(li); // 正誤判定関数を実行
});
choices.appendChild(li);
});
}
setQuiz();
回答フラグを管理する
今のままだと何度も回答できてしまうので、それをisAnsweredという変数を用意して管理します。
let currentNum = 0;
let isAnswered; // 変数名のみ用意
次に問題を表示する際は、isAnswered を false にし、正誤判定時に true にします。
function checkAnswer(li) {
if (isAnswered === true) { // 回答済みなら以下の処理をしない
return;
}
isAnswered = true; // 正誤判定 = 回答済みなので true に
if (li.textContent === quizSet[currentNum].c[0]) {
li.classList.add('correct');
} else {
li.classList.add('wrong');
}
}
function setQuiz() {
isAnswered = false; // 問題表示 = 未回答なので false に
question.textContent = quizSet[currentNum].q;
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => {
checkAnswer(li);
});
choices.appendChild(li);
});
}
setQuiz();
ちなみに
if (isAnswered === true) {
return;
}
は、
if (isAnswered) {
return;
}
と略されることがあるので覚えておく。
次の問題へ進む
次へ進むボタンはデフォルトではdisabledになっています。それを外すタイミングは回答した時、つまり正誤判定のところですね。
function checkAnswer(li) {
if (isAnswered === true) {
return;
}
isAnswered = true;
if (li.textContent === quizSet[currentNum].c[0]) {
li.classList.add('correct');
} else {
li.classList.add('wrong');
}
btn.classList.remove('disabled'); // disabledクラスを外す
}
次は表示された次へボタンにクリックイベントを追加して、次の問題へ進むようにします。
function setQuiz() {
isAnswered = false;
question.textContent = quizSet[currentNum].q;
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => {
checkAnswer(li);
});
choices.appendChild(li);
});
}
setQuiz();
btn.addEventListener('click', () => { // ボタンにクリックイベントを追加
currentNum++; // 出題問題をひとつ進める
setQuiz(); // 新たに問題表示
});
これで表示されるのですが、問題が入れ替わるのではなく下に追加表示されてしまいます。
これを解消します。
追加表示問題を解消する
選択肢は常に<ul id="choices">
直下に<li>
として生成されます。
setQuizが呼び出された段階で、一旦回答の<li>
を削除させてから、新たに選択肢<li>
を表示されることで解消します。
function setQuiz() {
isAnswered = false;
question.textContent = quizSet[currentNum].q;
while (choices.firstChild) { // choices の最初の子要素がある限り
choices.removeChild(choices.firstChild); // choices の最初の子要素を削除する
}
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => {
checkAnswer(li);
});
choices.appendChild(li);
});
}
setQuiz();
btn.addEventListener('click', () => {
currentNum++;
setQuiz();
});
こういう風に一旦削除してから表示させるというのは、他でもよく見かける処理だと思います。
プログラム作成の流れの一つして覚えておきたいですね。
ボタンの挙動を整理する
実はボタンが押せないように見えても押せてしまったり、次の問題へ行った時には表示状態だったりとまだ問題があります。これらも isAnswered と同じように、ボタンが disabled クラスを持っていたら動かないようになどという処理を追加してあげます。
btn.addEventListener('click', () => {
if (btn.classList.contains('disabled')) { // もしdisabledクラスをもっていたら
return; // 以下の処理をしない
}
btn.classList.add('disabled'); // 次の問題へ進みながら、disabledクラスを追加する
currentNum++;
setQuiz();
});
さらに最後の問題をクリックしたあとは「Next」ではなく「Show Score」にしてスコア表示を促しましょう。
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => {
checkAnswer(li);
});
choices.appendChild(li);
});
if (currentNum === quizSet.length - 1) { // 現在の問題数が最後だったら
btn.textContent = 'Show Score'; // ボタンのテキストを変更する
}
}
スコアを整理する
まずはスコア表示領域をhtmlで用意します。
ここではゲーム終了のボタンを押すと同時に表示されるのであれば何でも良いです。CSSでshowクラスを用意して、それが追加されたら表示されるようにしておいてください。
ついでにReplayボタンも表示しておきましょう。
<div class="container">
<p id="question"></p>
<ul id="choices"></ul>
<div id="btn" class="disabled">Next</div>
<section id="result">
<p></p>
<a href="">Replay?</a>
</section>
</div>
次はスコアとスコア表示領域のための定数を用意します。
const result = document.getElementById('result'); // スコア表示領域を取得
const scoreLabel = document.querySelector('#result > p'); // スコア表示エリアを取得
let currentNum = 0;
let isAnswered;
let score = 0; // デフォルトは0で
問題回答中は正解したらscoreを1増やします。
function checkAnswer(li) {
if (isAnswered === true) {
return;
}
isAnswered = true;
if (li.textContent === quizSet[currentNum].c[0]) {
li.classList.add('correct');
score++; // scoreを1増やす
} else {
li.classList.add('wrong');
}
btn.classList.remove('disabled');
}
もし最後の問題だったら、スコア表示領域を表示させ、スコアを計算して表示させます。
btn.addEventListener('click', () => {
if (btn.classList.contains('disabled')) {
return;
}
btn.classList.add('disabled');
if (currentNum === quizSet.length - 1) { // 最後の問題だったら
scoreLabel.textContent = `Score: ${score} / ${quizSet.length}`; // scoreを表示する
result.classList.add('show'); // スコア表示領域を表示させるためのshowクラスを追加する
} else { // 最後じゃなければ次に進む
currentNum++;
setQuiz();
}
});
ちなみにreplayはhrefが空なので、クリックすると再度同じページが読み込まれ最初に戻るようになります。
完成
以上で完成となります。これを作成するにあたり、JavaScriptを組むおおまかな流れというものがなんとなくわかった気がします。プログラムに慣れていないと、あっちにいったりこっちにいったりすることで難しく感じてしまっていたのですが、作る流れを把握することで、それもわずかながら理解できたと思います。
このプログラムは汎用性が高そうなので、今度はこれを利用してもう少し複雑なプログラムに挑戦したいです。
ありがとうございました。
'use strict';
{
const question = document.getElementById('question');
const btn = document.getElementById('btn');
const choices = document.getElementById('choices');
const result = document.getElementById('result');
const scoreLabel = document.querySelector('#result > p');
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;
let isAnswered;
let score = 0;
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;
}
function checkAnswer(li) {
if (isAnswered) {
return;
}
isAnswered = true;
if (li.textContent === quizSet[currentNum].c[0]) {
li.classList.add('correct');
score++;
} else {
li.classList.add('wrong');
}
btn.classList.remove('disabled');
}
function setQuiz() {
isAnswered = false;
question.textContent = quizSet[currentNum].q;
while (choices.firstChild) {
choices.removeChild(choices.firstChild);
}
const shuffledChoices = shuffle([...quizSet[currentNum].c]);
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.textContent = choice;
li.addEventListener('click', () => {
checkAnswer(li);
});
choices.appendChild(li);
});
if (currentNum === quizSet.length - 1) {
btn.textContent = 'Show Score';
}
}
setQuiz();
btn.addEventListener('click', () => {
if (btn.classList.contains('disabled')) {
return;
}
btn.classList.add('disabled');
if (currentNum === quizSet.length - 1) {
scoreLabel.textContent = `Score: ${score} / ${quizSet.length}`;
result.classList.add('show');
} else {
currentNum++;
setQuiz();
}
});
}