記憶ゲームの作り方:神経衰弱から音の順番まで
遊びをつくる執筆にClaudeを使用しています。

記憶ゲームを作ろうとすると、最初に「どのデータをどう管理するか」という設計の壁にぶつかります。カードをめくる神経衰弱も、光や音の順番を覚えるシーケンスゲームも、見た目は単純ですが「プレイヤーが覚えた状態」と「正解の状態」を正確に照合する仕組みが核心です。その仕組みを理解せずに作り始めると、バグが出たときに原因を特定できなくなります。 この記事では、記憶ゲームに共通する状態管理の考え方を整理したあと、神経衰弱とシーケンス記憶という二つの代表的な形式について、それぞれの実装ポイントと失敗しやすい箇所を具体的に説明します。HTML・JavaScriptで書けることを前提にしていますが、考え方はどの言語・ツールでも共通です。
この記事の目次
記憶ゲームに共通する「状態の三層構造」
記憶ゲームの設計でよく起きる混乱は、「正解データ」「現在の盤面・進行」「プレイヤーの入力履歴」の三つを一つのオブジェクトに混在させてしまうことです。これを分けて管理するだけで、バグの追跡が格段に楽になります。
- 正解データ:シャッフル後のカード配置、あるいは正解のシーケンス(色・音の順番)。ゲーム開始時に一度だけ生成し、以後は変更しません。
- 盤面・進行状態:何枚めくられているか、どこまでシーケンスが進んだか、などゲームの「現在地」です。
- プレイヤーの入力履歴:今ターンにクリックしたカード、今入力したシーケンスの途中経過です。
照合は「入力履歴」と「正解データ」を比べるだけにします。盤面の描画はその結果を「盤面・進行状態」に反映してから行います。この順番を守ると、照合ロジックとDOM操作が分離され、どちらかにバグが出ても切り分けやすくなります。
神経衰弱の実装:シャッフルと「めくり待ち」の制御
カード配置の生成
神経衰弱の正解データは「カードIDの配列」です。たとえば8ペア16枚なら、1〜8の数字を2個ずつ含む配列を用意し、Fisher-Yatesアルゴリズムでシャッフルします。ライブラリ不要で10行以内に書けます。
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const cards = shuffle([1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8]);ここで気をつけたいのは、Math.random()は疑似乱数なので厳密な乱数ではありませんが、ゲームの用途では十分です。
「2枚めくり待ち」の状態ロック
最もバグが起きやすいのは、2枚目のカードをめくった直後から判定が終わるまでの間です。この短い時間にプレイヤーが3枚目をクリックできてしまうと、状態が壊れます。
対策は単純で、「めくり中フラグ」を一つ用意するだけです。
let isLocked = false;
function onCardClick(index) {
if (isLocked) return;
if (flipped.includes(index)) return;
// めくり処理
if (flipped.length === 2) {
isLocked = true;
setTimeout(() => {
checkMatch();
isLocked = false;
}, 800);
}
}setTimeoutの時間は600〜1000ミリ秒が目安です。短すぎるとカードが戻るのを確認できず、長すぎると待ち時間がストレスになります。
難易度の調整ポイント
難易度を変えたいときに触るパラメータは三つです。
- ペア数、低難度:4〜6ペア、高難度:12〜20ペア
- カード表示時間(初期公開あり)、低難度:3〜5秒、高難度:なし
- めくり待ち時間、低難度:1000ms、高難度:500ms
「初期公開」とは、ゲーム開始直後に全カードを一瞬見せる演出です。これを入れるだけで初心者向けの難易度が大きく下がります。実装はゲーム開始時に全カードを表向きにし、setTimeoutで一定時間後に裏返すだけです。
シーケンス記憶の実装:提示と入力の分離
「サイモン」タイプのゲーム(色や音が光る順番を覚えて同じ順に押す)は、状態機械(ステートマシン)の考え方で設計すると整理しやすいです。
ゲームの状態は大きく三つに分けられます。
- 提示フェーズ:正解シーケンスをアニメーションで見せている間。プレイヤー入力を受け付けない。
- 入力フェーズ:プレイヤーが順番にボタンを押す間。アニメーションは走らせない。
- 判定フェーズ:入力完了後に正誤を判定し、次のラウンドへ遷移する間。
提示フェーズと入力フェーズを分けずに作ると、プレイヤーが提示中にボタンを押したときの処理が曖昧になります。状態変数を一つ持ち、現在のフェーズ名("showing" / "input" / "judging")を文字列で管理するのが最も追いやすい方法です。
シーケンスの提示アニメーション
シーケンスを順番に光らせるには、setTimeoutを再帰的に使うか、async/awaitで直列に処理するかの二択です。async/awaitのほうが読みやすくなります。
async function showSequence(sequence) {
gameState = "showing";
for (const item of sequence) {
highlight(item);
await wait(500);
unhighlight(item);
await wait(300);
}
gameState = "input";
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}ここで失敗しやすいのは、forループ内でsetTimeoutを直接使うケースです。setTimeoutはコールバックを非同期に呼ぶため、ループが先に終わってしまい全部同時に点滅します。awaitで直列化するか、インデックスにdelay * iをかけて時間をずらす必要があります。
音を組み合わせるとき
音の順番を覚えるゲームでは、WebAudio APIを使って音を鳴らせます。簡易な実装ならAudioContextで正弦波を生成し、周波数を変えるだけで異なる「音」を表現できます。音声ファイルを用意せずに済むので、ゲームの容量を増やさずに音要素を加えられます。
注意点として、ブラウザのオートプレイ制限により、ユーザー操作なしに音を鳴らすと初回は無音になります。ゲーム開始ボタンのクリック時にAudioContextを初期化すれば、以後は問題なく音が出ます。
両形式に共通する「記憶補助演出」の設計
記憶ゲームの面白さは「惜しい失敗」にあります。完全に覚えられず一瞬で終わるゲームはストレスになるので、プレイヤーが「もう少しで覚えられた」と感じられる演出が重要です。
神経衰弱では、不一致だったカードを0.8〜1秒表示してから裏返します。この時間が長すぎると間延びし、短すぎると確認できません。プレイヤーが自分でペアを見つけたときの「あ、あそこにあった」という感覚を損なわない範囲で調整します。
シーケンスゲームでは、入力ミス時に「どこまで正しかったか」を視覚的にフィードバックする方法があります。正解ボタンを緑に光らせ、ミスしたボタンを赤にするだけでも、プレイヤーは次のラウンドで「3番目でいつも間違える」と気づいて意識的に記憶しようとします。
もう一つ有効なのが「難易度の段階的上昇」です。神経衰弱ならペア数を徐々に増やす、シーケンスゲームならラウンドごとにシーケンスを1つずつ伸ばす、という構造は最初から設計に組み込んでおくと後で改修しやすくなります。
完成後のチェックリスト
公開前に確認しておきたい点を以下に挙げます。
- カードのシャッフル結果が毎回異なることを3〜5回起動して確認する
- 2枚目クリック直後に連打しても状態が壊れないかを試す(神経衰弱)
- 提示フェーズ中にボタンを押しても入力が無視されるかを試す(シーケンス)
- ゲームオーバー・クリア後に再プレイしたとき、前のゲームの状態が残っていないかを確認する
- スマートフォンのタッチ操作でも遅延なく動くかを確認する(clickイベントは300msの遅延が発生することがあるため、touchstartやpointerdownの使用を検討する)
再プレイ時のバグは見落としがちです。変数の初期化を一か所にまとめたresetGame()関数を作り、ゲーム開始時と再プレイ時の両方からそこを呼ぶ設計にすると安全です。
次の行動
神経衰弱から始める場合は、まず4ペア8枚・シャッフルなし(固定配置)で照合ロジックだけを動かし、正しく判定できることを確認してからシャッフルと演出を加えてください。シーケンスゲームは2色2ボタンで状態遷移を確認してからボタン数を増やすと、問題の切り分けが楽になります。完成したゲームはHTMLファイルまたはZIPにまとめてアソバースに投稿できます。

