ガイドメニュー

シャッフルモード:アルゴリズムと実装

「Concentration - 究極の音の記憶ゲーム」のシャッフルモードは、5n-1回試行(4,9,14…)で2枚のカードを告知し、5n回試行(5,10,15…)でその2枚を交換する機能です。表(めくられた状態)でも交換可能で、プレイヤーの記憶を揺さぶります。告知は紫枠と点滅で強調、交換はスライドアニメーションと「SHUFFLE!」表示、600Hz三角波のサウンドで視覚・聴覚を刺激します。このページでは、シャッフルモードのアルゴリズム、ソースコード、仕組み、他の機能との干渉回避策を詳しく解説します。また、告知点滅やスライドアニメーションを再生ボタンで体験し、視覚的な効果を比較できます。このインタラクティブな体験を通じて、シャッフルモードの技術を深く理解しましょう。

アルゴリズム

目的:定期的に2枚のカードを交換し、事前に告知してプレイヤーの戦略を試す。表/裏状態を保持し、滑らかなスライドアニメーションとサウンドで直感的な操作を提供する。他の機能(カードめくり、コンボ)とスムーズに連携し、競合を防ぐ。

手順

  1. 告知(5n-1回試行):`selectShufflePair`関数で、未マッチのカードから2枚をランダムに選択。`shuffle-highlight`クラスを追加し、紫枠と点滅で告知。`shufflePair`配列に保存。
  2. 交換(5n回試行):`executeShuffle`関数で、告知済みの2枚を確認。マッチ済みならスキップ。仮要素でスライドアニメーション(0.5秒)を実行、DOMで単一スワップ、表/裏状態を保持。「SHUFFLE!」オーバーレイと600Hz三角波を再生。
  3. 試行数監視:`checkMatch`内で試行数(`attempts`)をチェック、5n-1回で告知、5n回で交換をトリガー。
  4. 状態管理:`shufflePair`をクリア、交換後のカード状態を更新、`isGameActive`で操作を制御。

ソースコード

以下は、シャッフルモードの主要コード(`game.js`)です。完全版は`game.js`を参照してください。コードは`white-space: pre-wrap`で整形済み、改行やインデントがブラウザで正確に表示されます。

function selectShufflePair() { const unmatchedCards = cards.filter(card => !card.classList.contains("matched")); if (unmatchedCards.length < 2) return []; const selectedCards = unmatchedCards.sort(() => Math.random() - 0.5).slice(0, 2); selectedCards.forEach(card => card.classList.add("shuffle-highlight")); return selectedCards; } async function executeShuffle() { isGameActive = false; shuffleOverlay.style.display = "block"; playShuffleSound(); setTimeout(() => shuffleOverlay.style.display = "none", 1000); if (shufflePair.length === 2) { const [card1, card2] = shufflePair; if (!card1.classList.contains("matched") && !card2.classList.contains("matched")) { card1.classList.add("shuffle-highlight"); card2.classList.add("shuffle-highlight"); const card1Index = Array.from(gameBoard.children).indexOf(card1); const card2Index = Array.from(gameBoard.children).indexOf(card2); const card1Rect = card1.getBoundingClientRect(); const card2Rect = card2.getBoundingClientRect(); const tempCard1 = card1.cloneNode(true); const tempCard2 = card2.cloneNode(true); tempCard1.style.position = "absolute"; tempCard2.style.position = "absolute"; tempCard1.style.top = `${card1Rect.top}px`; tempCard1.style.left = `${card1Rect.left}px`; tempCard2.style.top = `${card2Rect.top}px`; tempCard2.style.left = `${card2Rect.left}px`; tempCard1.style.transition = "all 0.5s ease"; tempCard2.style.transition = "all 0.5s ease"; document.body.appendChild(tempCard1); document.body.appendChild(tempCard2); await new Promise(resolve => setTimeout(() => { tempCard1.style.top = `${card2Rect.top}px`; tempCard1.style.left = `${card2Rect.left}px`; tempCard2.style.top = `${card1Rect.top}px`; tempCard2.style.left = `${card1Rect.left}px`; setTimeout(resolve, 500); }, 100)); const parent = gameBoard; const card1Next = card1.nextSibling; const card2Next = card2.nextSibling; if (card1Index < card2Index) { parent.insertBefore(card2, card1Next); parent.insertBefore(card1, card2Next || null); } else { parent.insertBefore(card1, card2Next); parent.insertBefore(card2, card1Next || null); } tempCard1.remove(); tempCard2.remove(); card1.style.visibility = "visible"; card2.style.visibility = "visible"; card1.classList.remove("shuffle-highlight"); card2.classList.remove("shuffle-highlight"); } } shufflePair = []; isGameActive = true; }

CSSアニメーション(`guide.css`):告知点滅とスライドを実現。

.shuffle-highlight { border: 4px solid #800080; box-shadow: 0 0 15px #800080, 0 0 5px #ffffff inset; transform: scale(1.1); animation: blink 0.5s infinite alternate; } @keyframes blink { 0% { opacity: 1; } 100% { opacity: 0.7; } } #shuffleOverlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-5deg); font-size: 90px; color: #00ff00; text-shadow: 4px 4px 8px rgba(0,0,0,0.8); animation: comboPulse 1s ease-out forwards; } @keyframes comboPulse { 0% { opacity: 1; transform: translate(-50%, -50%) rotate(-5deg) scale(1); } 50% { opacity: 0.7; transform: translate(-50%, -50%) rotate(-5deg) scale(1.4); } 100% { opacity: 0; transform: translate(-50%, -50%) rotate(-5deg) scale(1.8); } }

アニメーションを体感する

シャッフルモードの告知点滅とスライドアニメーションは、CSSとJavaScriptを組み合わせて、プレイヤーの注意を引き、記憶を揺さぶります。以下では、告知点滅(紫枠)、スライド(カード交換)、フェード(オーバーレイ)のアニメーションを比較し、視覚的に理解できるようにサンプルを用意しました。ボタンをクリックして、各アニメーションを体験してください。告知点滅の強調感やスライドの動きに注目し、効果の違いを体感しましょう。各アニメーションには、シャッフル音(600Hz三角波)が付加され、聴覚も刺激します。

🔥
🔥
🔥
  • 告知点滅(ゲーム使用):紫枠と点滅でカードを強調、シャッフル告知に使用。注意を引き、記憶に残る効果。
  • スライド(ゲーム使用):2枚のカードをスライドして交換。動きが明確で、交換を視覚化。
  • フェード:カードをフェードイン/アウト。柔らかい効果だが、交換の動きは弱い。

解説

  • 告知点滅:`animation: blink 0.5s infinite alternate`で0.5秒ごとに明滅、紫枠(`border: 4px solid #800080`)とスケール(`transform: scale(1.1)`)で強調。ゲームでは、告知時の視覚的インパクトが強い。
  • スライド:`left`プロパティを0~50pxで変化させ、2枚のカードを交換。動きが明確で、交換の視覚化に最適。ゲームの標準効果。
  • フェード:`opacity`を0~1で変化、柔らかい効果。交換の動きを表現するには弱いため、ゲームでは不採用。

ボタンを押してアニメーションを比較し、点滅とスライドがシャッフルモードに最適な理由(強調感、動きの明確さ)を体感してください。この体験を通じて、アニメーションの選択がプレイヤーの記憶や操作性にどう影響するかを理解できます。

機能の仕組み

データ構造

  • `shufflePair`:告知済みの2枚を配列で保持、交換対象を管理。
  • `cards`:全カードの配列、状態(`matched`、`flipped`)を監視。
  • `attempts`:試行数、告知(5n-1)や交換(5n)のトリガーに使用。

制御フロー

  • 告知:`selectShufflePair`でランダム選択、ハイライト追加、`shufflePair`に保存。
  • 交換:`executeShuffle`で仮要素を生成、スライドアニメーション後、DOMでスワップ。
  • トリガー:`checkMatch`内で試行数を監視、告知・交換を条件分岐で実行。
  • 終了:仮要素を削除、`shufflePair`をクリア、`isGameActive`を復帰。

干渉回避の工夫

他の機能との競合を防ぐための対策を以下に示します:

  • カードめくりとの連携:`isGameActive`でシャッフル中はクリックを無効化、交換後も表/裏状態を保持。スライドアニメーション(0.5秒)はめくり(0.5秒)と独立。
  • コンボとの連携:シャッフルはコンボに影響せず、`checkMatch`内で独立処理。コンボ表示は別`div`(`comboOverlay`)で干渉なし。
  • サウンドとの連携:600Hz三角波(0.5秒)は短時間、めくりやコンボ音(0.3~0.4秒)と重複しない。独立した`AudioContext`で管理。
  • バグ対策:初期の再移動バグは単一スワップで解消、インデックスずれは`Array.from`で動的取得。仮要素の削除でDOM肥大化を防止。

学習ポイント

  • ランダム選択:Fisher-Yatesシャッフルを応用したカード選択アルゴリズムを理解。
  • 非同期アニメーション:`async/await`と仮要素でスライドアニメーションを制御する方法を学ぶ。
  • DOM操作:`insertBefore`を使った正確なカード交換と、仮要素の管理を習得。
  • 状態管理:`isGameActive`や`shufflePair`で機能間の競合を防ぐ設計を学ぶ。