アナグラムクイズの作り方 — JavaScriptでブラウザゲームを作ろう

このゲームで遊ぶ → ソースコード全文 →

このチュートリアルで学べること

アナグラムクイズは、バラバラになった文字を並べ替えて元の単語を当てるゲームです。このゲームを作ることで、配列のシャッフル・タイマー処理・動的なDOM操作といったJavaScriptの実践的なテクニックが身につきます。

ステップ1: 単語をシャッフルしてタイル表示する

アナグラムの核心は「単語の文字をランダムに並び替える」ことです。ランダム性を保証するために、Fisher-Yatesアルゴリズムというシャッフル手法を使います。また、元の単語と同じ並びにならないよう繰り返しチェックします。

// Fisher-Yatesアルゴリズムで配列をシャッフル
function shuffle(arr) {
  var a = arr.slice();
  for (var i = a.length - 1; i > 0; i--) {
    var j = Math.floor(Math.random() * (i + 1));
    var tmp = a[i];
    a[i] = a[j];
    a[j] = tmp;
  }
  return a;
}

// 元の単語と違う並びになるまでシャッフルを繰り返す
function scrambleWord(word) {
  var chars = word.split('');
  if (chars.length <= 1) return word;
  var attempts = 0;
  var result;
  do {
    result = shuffle(chars).join('');
    attempts++;
  } while (result === word && attempts < 50);
  return result;
}

arr.slice() で元の配列をコピーしてからシャッフルしているのがポイントです。元のデータを壊さずに新しい並びを作れます。do...while ループで、元と同じ並びになってしまった場合は再度シャッフルします。

シャッフルした文字を1文字ずつタイルとして表示する

バラバラになった文字は、1文字ずつ<span>要素として作り、アニメーション遅延を付けて順番に表示します。

function renderTiles(text) {
  tilesAreaEl.innerHTML = '';
  var chars = text.split('');
  chars.forEach(function (ch, i) {
    var tile = document.createElement('span');
    tile.className = 'tile';
    tile.textContent = ch;
    // 文字ごとに少しずつ表示タイミングをずらす
    tile.style.animationDelay = (i * 0.08) + 's';
    tilesAreaEl.appendChild(tile);
  });
}

animationDelayをインデックスに応じて変えることで、文字が左から順番にポップするアニメーションが実現できます。CSSアニメーションとJavaScriptの連携の基本テクニックです。

ステップ2: 選択肢の生成とタイマー処理

選択肢は「正解1つ+不正解3つ」の計4択です。不正解は同じカテゴリの単語を優先して選ぶことで、よりゲームらしい難易度になります。そしてsetIntervalでタイマーを動かします。

// 同カテゴリ優先で不正解選択肢を生成
function getWrongChoices(correctEntry, count) {
  var sameCategory = [];
  var otherCategory = [];
  WORDS.forEach(function (w) {
    if (w.word === correctEntry.word) return;
    if (w.category === correctEntry.category) {
      sameCategory.push(w.word);
    } else {
      otherCategory.push(w.word);
    }
  });
  sameCategory = shuffle(sameCategory);
  otherCategory = shuffle(otherCategory);

  var result = [];
  // 同カテゴリから最大2つ選ぶ
  while (result.length < Math.min(count, 2) && sameCategory.length > 0) {
    result.push(sameCategory.pop());
  }
  // 残りは他カテゴリから補充
  while (result.length < count && otherCategory.length > 0) {
    result.push(otherCategory.pop());
  }
  return result;
}

同カテゴリの単語を不正解に混ぜることで「チョコレート」のアナグラムに「ケーキ」や「プリン」が選択肢として出るようになります。全然違うカテゴリより迷いやすくなりますね。

setIntervalでカウントダウンタイマーを作る

var TIME_LIMIT = 15; // 秒
var CATEGORY_HINT_DELAY = 5; // 5秒後にヒント表示

function startTimer() {
  timeLeft = TIME_LIMIT;
  timerStart = Date.now();
  timerBarEl.style.width = '100%';

  clearInterval(timer);
  timer = setInterval(function () {
    var elapsed = (Date.now() - timerStart) / 1000;
    timeLeft = Math.max(0, TIME_LIMIT - elapsed);
    var pct = (timeLeft / TIME_LIMIT) * 100;
    timerBarEl.style.width = pct + '%';

    // 残り時間に応じてバーの色を変える
    if (timeLeft <= 3) {
      timerBarEl.classList.add('danger');
    } else if (timeLeft <= 7) {
      timerBarEl.classList.add('warning');
    }

    // 5秒経過したらカテゴリヒントを表示
    if (elapsed >= CATEGORY_HINT_DELAY && !categoryHintEl.classList.contains('visible')) {
      categoryHintEl.textContent = 'ヒント: ' + currentWord.category;
      categoryHintEl.classList.add('visible');
    }

    if (timeLeft <= 0) {
      clearInterval(timer);
      handleTimeout();
    }
  }, 50);
}

Date.now()で開始時刻を記録し、経過時間を計算してタイマーバーの幅をパーセントで更新しています。setIntervalの間隔を50ミリ秒にすることで、なめらかなアニメーションになります。

ステップ3: 回答判定とスコア計算を実装しよう

プレイヤーが選択肢をクリックしたとき、正解かどうかを判定してスコアを計算します。スコアは「基礎100点+残り時間×10点」のボーナス方式です。

function handleAnswer(chosen, btnEl) {
  answered = true;
  stopTimer();

  var isCorrect = chosen === currentWord.word;
  var buttons = choicesEl.querySelectorAll('.choice-btn');

  if (isCorrect) {
    btnEl.classList.add('correct');
    // タイムボーナス: 残り秒数 × 10点
    var timeBonus = Math.round(timeLeft * 10);
    var gained = 100 + timeBonus;
    score += gained;
    correctCount++;
    scoreEl.textContent = score;

    roundResultEl.innerHTML =
      '<span class="result-correct">⭐ +' + gained + '点(ボーナス +' + timeBonus + ')</span>';

    // 正解タイルにアニメーションクラスを付ける
    var tiles = tilesAreaEl.querySelectorAll('.tile');
    tiles.forEach(function (t) { t.classList.add('correct'); });
  } else {
    btnEl.classList.add('wrong');
    // 正解ボタンをハイライト表示
    buttons.forEach(function (b) {
      if (b.textContent === currentWord.word) {
        b.classList.add('correct');
      }
    });
  }

  // すべてのボタンを無効化して誤クリックを防ぐ
  buttons.forEach(function (b) { b.disabled = true; });

  // 2秒後に次の問題へ自動的に進む
  setTimeout(function () {
    nextQuestion();
  }, 2000);
}

chosen === currentWord.wordという文字列の比較で正誤判定ができます。不正解のとき正解ボタンをハイライトすることで、プレイヤーが答えを確認できます。disabled = trueでボタンをグレーアウトして連打を防止するのも大切です。

ゲーム終了時のベストスコア保存

var bestScore = parseInt(localStorage.getItem('anagram-quiz-best') || '0', 10);

function showEndScreen() {
  // 星評価(正解数ベース)
  var stars = 0;
  if (correctCount >= 9) stars = 3;
  else if (correctCount >= 6) stars = 2;
  else if (correctCount >= 3) stars = 1;

  // ベストスコア更新
  if (score > bestScore) {
    bestScore = score;
    localStorage.setItem('anagram-quiz-best', bestScore);
  }
  endScreen.classList.add('active');
}

localStorageはブラウザにデータを保存できる仕組みです。ページを閉じてもデータが消えないので、ベストスコアの保存に最適です。parseIntで文字列を数値に変換するのを忘れずに。

まとめ — 次のステップ

おつかれさまでした!ここまででアナグラムクイズの基本が完成しました。Fisher-Yatesシャッフルで文字を並び替え、タイマーで制限時間を作り、タイムボーナスでスコアに緊張感を出しました。さらに発展させるなら、英語の単語も追加したり、難易度ごとに制限時間を変えたり、Web Speech APIで音声読み上げにも挑戦してみましょう。

よくある質問

Q: Fisher-Yatesアルゴリズムとは何ですか?

A: 配列をランダムに並び替えるアルゴリズムです。後ろから順番に、自分より前のランダムな位置と入れ替えていきます。「Math.random()で毎回全体をソートする」方法より偏りなく公平にシャッフルできるため、ゲームや抽選などでよく使われます。

Q: どのくらい時間がかかりますか?

A: 基本部分は約30分〜1時間で作れます。単語リストを増やしたり音声読み上げ(Web Speech API)を追加したりすると、さらに楽しく発展させられます。