【ゲーム】JavaScript:14 ペアマッチゲーム

 「🎯 ペアマッチゲーム 🎯」は、裏返しのカードから同じ絵文字をペアでめくり、できるだけ早くすべてのペアを揃えてクリアを目指す、一人用の神経衰弱系ゲームです。

遊び方と操作方法

  • 自動でゲーム開始
    ページ読み込み時または「🔄再挑戦」ボタン押下でゲームが初期化され、8種類×2枚ずつ、合計16枚のカードがシャッフルされて4×4のグリッドに裏向きで配置されます。
  • カードをめくる
    カードをクリックすると表向きになり、絵文字が表示されます。
  • ペア判定
    ・1枚目をめくった後、もう1枚をめくります。
    ・同じ絵文字ならペア成功→カードに「matched」クラスが付き、緑色に固定。
    ・違う絵文字なら1秒後に自動で裏返し。
  • タイマーとスコア
    ・クリックと同時に経過タイマーがスタート(秒表示)。
    ・すべてのペアを揃えたタイミングでタイマーを停止し、経過秒数をスコアとして記録します。
  • スコアボード
    クリアタイム(秒)が localStorage に保存され、昇順でハイスコア一覧として表示されます。

ルール

  • 合計16枚(8ペア)のカードをめくってペアを探します。
  • 連続で正解(ペア成功)してもボーナスはありません。
  • ペアをすべて揃えた瞬間にゲームクリア。
  • タイマーが動き続けるため、いかに早くすべてのペアを揃えるかが勝負です。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

14 ペアマッチゲーム

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

pairs_match_bg.png

ゲーム画面イメージ

プログラム全文(pairs_match.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🎯ペアマッチゲーム🎯</title>
    <style>
        /* 背景画像を往復スクロールする設定 */
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin: 0;
            padding: 0;
            background: url('pairs_match_bg.png') repeat-x 0 0 fixed;
            background-size: cover;
            animation: scrollBG 30s linear infinite;
            color: #fff;
        }
        @keyframes scrollBG {
            0% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
            100% { background-position: 0% 50%; }
        }

        /* タイトル */
        h1 {
            margin-top: 20px;
            font-size: 32px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
        }

        /* タイマー表示 */
        #timer {
            margin-top: 15px;
            font-size: 24px;
            font-weight: bold;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
        }

        /* メッセージエリア */
        .message-area {
            margin-top: 20px;
            font-size: 28px;
            color: #ffeb3b;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
        }

        /* ゲームボード */
        .game-board {
            display: grid;
            grid-template-columns: repeat(4, 150px);
            gap: 10px;
            justify-content: center;
            margin: 20px auto;
        }

        /* カード */
        .card {
            width: 150px;
            height: 150px;
            background-color: #007acc;
            color: white;
            font-size: 48px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 10px;
            cursor: pointer;
            user-select: none;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
        }
        .card:hover {
            transform: scale(1.1);
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
        }
        .flipped {
            background-color: #fff;
            color: #000;
            cursor: default;
        }
        .matched {
            background-color: #32cd32;
            color: white;
            cursor: default;
        }

        /* 再挑戦ボタン */
        .retry-btn {
            margin-top: 20px;
            padding: 12px 24px;
            background: linear-gradient(45deg, #4caf50, #2e7d32);
            color: #fff;
            border: none;
            border-radius: 8px;
            font-size: 20px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .retry-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
        }

        /* スコアボード */
        .scoreboard {
            margin-top: 30px;
        }
        .scoreboard h2 {
            font-size: 24px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
        }
        .scoreboard ul {
            list-style: none;
            padding: 0;
            color: #fff;
            font-size: 18px;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
        }
    </style>
</head>
<body>
    <h1>🎯ペアマッチゲーム🎯</h1>
    <div id="timer">⏱経過時間: 0秒</div>
    <div id="messageArea" class="message-area"></div>
    <div class="game-board" id="gameBoard"></div>
    <button class="retry-btn" id="retryButton">🔄再挑戦</button>
    <div class="scoreboard">
        <h2>📊スコアボード📊</h2>
        <ul id="scoreList"></ul>
    </div>

    <script>
        // --- 要素取得 ---
        const gameBoard    = document.getElementById('gameBoard');
        const retryButton  = document.getElementById('retryButton');
        const timerDisplay = document.getElementById('timer');
        const scoreList    = document.getElementById('scoreList');
        const messageArea  = document.getElementById('messageArea');

        // --- カードの種類(絵文字) ---
        const cardValues = ['🍎', '🍌', '🍇', '🍒', '🍓', '🍍', '🥝', '🍋'];

        // --- ゲームの状態管理変数 ---
        let firstCard     = null;    // 1枚目にめくったカード
        let secondCard    = null;    // 2枚目にめくったカード
        let preventClick  = false;   // クリックを一時停止するフラグ
        let timer;                   // setInterval の ID
        let elapsedTime   = 0;       // 経過秒数
        let matchedPairs  = 0;       // 揃ったペアの数

        /**
         * タイマーをスタートする
         */
        function startTimer() {
            elapsedTime = 0;
            timerDisplay.textContent = `⏱経過時間: ${elapsedTime}秒`;
            timer = setInterval(() => {
                elapsedTime++;
                timerDisplay.textContent = `⏱経過時間: ${elapsedTime}秒`;
            }, 1000);
        }

        /**
         * タイマーをストップする
         */
        function stopTimer() {
            clearInterval(timer);
        }

        /**
         * スコアをローカルに保存する
         */
        function saveScore() {
            const scores = JSON.parse(localStorage.getItem('pairMatchScores')) || [];
            scores.push(elapsedTime);
            localStorage.setItem('pairMatchScores', JSON.stringify(scores));
            updateScoreboard();
        }

        /**
         * スコアボードを更新して表示する
         */
        function updateScoreboard() {
            const scores = JSON.parse(localStorage.getItem('pairMatchScores')) || [];
            scoreList.innerHTML = scores
                .sort((a, b) => a - b)
                .map(score => `<li>${score}秒</li>`)
                .join('');
        }

        /**
         * ゲームを初期化・開始する
         */
        function initializeGame() {
            // メッセージをクリア
            messageArea.textContent = '';
            // ボードをクリア
            gameBoard.innerHTML = '';
            matchedPairs = 0;

            // タイマー再スタート
            stopTimer();
            startTimer();

            // カードをペアで用意してシャッフル
            let cards = [...cardValues, ...cardValues];
            cards.sort(() => 0.5 - Math.random());

            // カード要素を生成
            cards.forEach(value => {
                const card = document.createElement('div');
                card.classList.add('card');
                card.dataset.value = value;
                card.textContent = ''; // めくる前は空
                gameBoard.appendChild(card);

                // クリック時の処理
                card.addEventListener('click', () => {
                    // 既に2枚めくっている or 同じカード or マッチ済みは無視
                    if (preventClick || card.classList.contains('flipped') || card.classList.contains('matched')) return;

                    // カードをめくる
                    card.textContent = card.dataset.value;
                    card.classList.add('flipped');

                    if (!firstCard) {
                        // 最初のカードを保持
                        firstCard = card;
                    } else {
                        // 2枚目のカードを保持
                        secondCard = card;
                        preventClick = true;

                        // ペア判定
                        if (firstCard.dataset.value === secondCard.dataset.value) {
                            // マッチ成功
                            firstCard.classList.add('matched');
                            secondCard.classList.add('matched');
                            matchedPairs++;
                            // 全ペアクリア時の処理
                            if (matchedPairs === cardValues.length) {
                                stopTimer();
                                saveScore();
                                messageArea.textContent = `🎉ゲームクリア!経過時間: ${elapsedTime}秒🎉`;
                            }
                            resetSelection();
                        } else {
                            // 不一致なら1秒後に裏返す
                            setTimeout(() => {
                                firstCard.textContent = '';
                                secondCard.textContent = '';
                                firstCard.classList.remove('flipped');
                                secondCard.classList.remove('flipped');
                                resetSelection();
                            }, 1000);
                        }
                    }
                });
            });
        }

        /**
         * カード選択状態をリセット
         */
        function resetSelection() {
            firstCard = null;
            secondCard = null;
            preventClick = false;
        }

        // 「再挑戦」ボタンでゲーム再スタート
        retryButton.addEventListener('click', initializeGame);

        // 初回ロード時にゲームを開始&スコアボードを更新
        initializeGame();
        updateScoreboard();
    </script>
</body>
</html>

アルゴリズムの流れ

ステップ関数/処理詳細
ゲーム開始・初期化initializeGame()ペア用カード16枚生成→シャッフル→盤面HTML再構築→タイマー初期化→変数リセット
タイマー開始startTimer()setInterval で1秒ごとに elapsedTime++→表示更新
カードクリッククリックイベントハンドラ未めくりかつ2枚未選択なら絵文字表示&flippedクラス追加
ペア判定if (firstCard && secondCard)2枚めくり後に値を比較→一致なら matched クラス → ペアカウント++ → 全ペアならクリア処理
裏返しsetTimeout(...,1000)不一致時、1秒待ってから textContent=''flipped 削除
ペア成功時matchedPairs++カウントし、必要数到達で stopTimer() & saveScore()→結果メッセージ
スコア保存・表示saveScore(), updateScoreboard()localStorage へ経過秒を蓄積→降順ソートしてリストに描画
再挑戦ボタン押下で initializeGame()再初期化・再挑戦

関数の詳細

関数名役割
initializeGame()ゲームを初期化しカード配置・タイマー再スタート・変数のクリアを行う
startTimer()elapsedTime を秒単位でカウントし、画面上に経過時間を表示
stopTimer()clearInterval でタイマーを停止
saveScore()localStorage にクリアタイムを配列保存し、スコアボード更新を呼び出す
updateScoreboard()保存されたスコアを取得し昇順ソート→HTMLリストに描画
カードクリックハンドラカードをめくって flipped クラスを追加→2枚めくり後にペア判定→一致時/不一致時の処理
resetSelection()firstCard, secondCard, preventClick をリセット

このゲームの改造ポイント

  • 難易度調整:カードの種類数(ペア数)やグリッドサイズ(4×4→6×6等)を変更し、難易度を上げる。
  • 制限時間バリエーション:制限時間を設定し、クリア条件を「時間内に全ペア」または「何ペア揃えられるか」に変更。
  • サウンドエフェクト:めくる音、ペア成功音、失敗音などの効果音を追加して臨場感アップ。
  • テーマ切替:絵文字以外にキャラクターや写真をカードに使用するカスタムデッキ機能を実装。
  • マルチプレイヤーモード:複数人が交互にめくる対戦モードを追加し、得点制バトルに拡張。
  • アニメーション強化:カードめくり時に3D回転エフェクト、ペア成功時のアニメーションを追加。
  • デイリーチャレンジ:毎日異なるシャッフルシードを生成し、共通タイムでランキングを競う機能。

 この基本構造をベースに、ぜひオリジナル要素や演出を追加して、魅力的な「ペアマッチ」ゲームに仕上げてみてください!