神経衰弱(メモリーマッチ)は、裏返されたカードを2枚ずつめくって同じ絵柄のペアを探すゲームです。記憶力を使うシンプルなルールですが、配列のシャッフル、カードの状態管理、マッチング判定など、プログラミングの基本がたくさん詰まっています。難易度設定や星評価の仕組みも学べます。
まずは絵文字のペアを作ってシャッフルしましょう。Fisher-Yatesアルゴリズムは、配列を偏りなくランダムに並び替える有名な手法です。難易度ごとにカードの枚数を変える仕組みも用意します。
// 配列をシャッフル(Fisher-Yates)
function shuffle(array) {
var arr = array.slice();
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
配列の末尾から順に、ランダムな位置の要素と入れ替えていきます。array.slice()でコピーを作ってからシャッフルするので、元の配列は変更されません。このアルゴリズムは偏りのない完全なランダム並べ替えを保証します。
var DIFFICULTY_CONFIG = {
easy: { rows: 3, cols: 4, pairs: 6 },
normal: { rows: 4, cols: 4, pairs: 8 },
hard: { rows: 4, cols: 6, pairs: 12 }
};
function createCards() {
var config = DIFFICULTY_CONFIG[currentDifficulty];
totalPairs = config.pairs;
// 絵文字をシャッフルして必要な数だけ取得
var shuffledEmoji = shuffle(EMOJI_POOL);
var selectedEmoji = shuffledEmoji.slice(0, totalPairs);
// ペアを作る(各絵文字を2つずつ)
var cardValues = [];
for (var i = 0; i < selectedEmoji.length; i++) {
cardValues.push(selectedEmoji[i]);
cardValues.push(selectedEmoji[i]);
}
// シャッフル
cardValues = shuffle(cardValues);
return cardValues;
}
難易度ごとにペア数が異なります(かんたん6組、ふつう8組、むずかしい12組)。24種類の絵文字プールからランダムに選び、各絵文字を2枚ずつ配置します。シャッフルが2回登場するのがポイントで、1回目は使う絵文字の選択、2回目はカードの配置場所をランダムにしています。
カードをクリックしたらめくり、2枚めくったところでペアが一致するかを判定します。既にめくられたカードやマッチ済みのカードは無視する必要があります。
var flippedCards = [];
var isLocked = false;
function onCardClick(e) {
// ロック中は何もしない
if (isLocked) return;
var cardEl = e.currentTarget;
var id = parseInt(cardEl.getAttribute('data-id'), 10);
var card = cards[id];
// 既にめくられている or マッチ済みは無視
if (card.isFlipped || card.isMatched) return;
// 同じカードの二重クリック防止
if (flippedCards.length === 1 && flippedCards[0].id === id) return;
// 最初のカードをめくった時にタイマー開始
if (!gameStarted) {
gameStarted = true;
startTimer();
}
// カードをめくる
card.isFlipped = true;
cardEl.classList.add('flipped');
flippedCards.push(card);
// 2枚めくった場合
if (flippedCards.length === 2) {
moves++;
movesEl.textContent = moves;
var card1 = flippedCards[0];
var card2 = flippedCards[1];
if (card1.value === card2.value) {
handleMatch(card1, card2);
} else {
handleMismatch(card1, card2);
}
}
}
isLockedフラグは、2枚めくって比較中のときに別のカードをめくれないようにするためのものです。flippedCards配列に現在めくられているカードを保持し、2枚になったらvalueを比較します。タイマーは最初のカードをめくった瞬間に開始するので、考える時間が無駄になりません。
ペアが一致した場合はカードを表のまま固定し、不一致の場合は少し見せてから裏に戻します。この「ちょっとだけ見せる」時間が神経衰弱の醍醐味です。
function handleMatch(card1, card2) {
card1.isMatched = true;
card2.isMatched = true;
matchedPairs++;
// マッチのDOM更新
var el1 = cardGrid.querySelector('[data-id="' + card1.id + '"]');
var el2 = cardGrid.querySelector('[data-id="' + card2.id + '"]');
setTimeout(function () {
if (el1) el1.classList.add('matched');
if (el2) el2.classList.add('matched');
}, 300);
flippedCards = [];
// 全ペア完了チェック
if (matchedPairs === totalPairs) {
setTimeout(function () {
handleGameComplete();
}, 600);
}
}
function handleMismatch(card1, card2) {
isLocked = true;
cardGrid.classList.add('locked');
// 800ms後にカードを裏返す
setTimeout(function () {
card1.isFlipped = false;
card2.isFlipped = false;
var el1 = cardGrid.querySelector('[data-id="' + card1.id + '"]');
var el2 = cardGrid.querySelector('[data-id="' + card2.id + '"]');
if (el1) el1.classList.remove('flipped');
if (el2) el2.classList.remove('flipped');
flippedCards = [];
isLocked = false;
cardGrid.classList.remove('locked');
}, 800);
}
マッチしたカードはisMatched = trueに設定し、表向きのまま固定します。matchedクラスでキラキラするアニメーションも付けられます。ミスマッチの場合は800ミリ秒間だけ表を見せてから裏に戻し、その間はisLocked = trueで操作をブロックします。
function calculateStars(moves, pairs) {
var ratio = moves / pairs;
if (ratio <= 1.5) return 3; // 星3: 手数がペア数の1.5倍以内
if (ratio <= 2.5) return 2; // 星2: 手数がペア数の2.5倍以内
return 1; // 星1: それ以上
}
function saveBestScore(difficulty, moves, time) {
var current = getBestScore(difficulty);
// 手数が少ない方が良い。同じならタイムが短い方が良い
if (!current || moves < current.moves ||
(moves === current.moves && time < current.time)) {
localStorage.setItem(
bestScoreKey(difficulty),
JSON.stringify({ moves: moves, time: time })
);
return true; // 新記録
}
return false;
}
ペア数に対する手数の比率で星を決めています。normalモード(8ペア)なら、12手以内で星3、20手以内で星2です。ベストスコアは難易度ごとにlocalStorageに保存し、手数が最優先、同じなら時間が短い方がベストとなります。JSON.stringifyで複数の値をまとめて保存しているのもポイントです。
おつかれさまでした!神経衰弱の基本が完成しました。Fisher-Yatesシャッフル、カードの状態管理とマッチング判定、操作ロック、星評価システムを学びました。発展として、カードをめくるときの3Dフリップアニメーションをより凝ったものにしたり、テーマ(動物・食べ物など)を切り替えられるようにしたり、オンライン対戦モードを実装してみるのも面白いですよ。
A: はい、大丈夫です。このチュートリアルではステップごとにコードを書いていくので、初めての方でも順番に進めれば完成できます。わからないところがあれば、ひなテックの教室で質問もできますよ。
A: 基本部分は約30分〜1時間で作れます。見た目をこだわったり機能を追加すると、さらに楽しく発展させられます。