「Concentration - 究極の音の記憶ゲーム」のシャッフルモードは、5n-1回試行(4,9,14…)で2枚のカードを告知し、5n回試行(5,10,15…)でその2枚を交換する機能です。表(めくられた状態)でも交換可能で、プレイヤーの記憶を揺さぶります。告知は紫枠と点滅で強調、交換はスライドアニメーションと「SHUFFLE!」表示、600Hz三角波のサウンドで視覚・聴覚を刺激します。このページでは、シャッフルモードのアルゴリズム、ソースコード、仕組み、他の機能との干渉回避策を詳しく解説します。また、告知点滅やスライドアニメーションを再生ボタンで体験し、視覚的な効果を比較できます。このインタラクティブな体験を通じて、シャッフルモードの技術を深く理解しましょう。
アルゴリズム
目的:定期的に2枚のカードを交換し、事前に告知してプレイヤーの戦略を試す。表/裏状態を保持し、滑らかなスライドアニメーションとサウンドで直感的な操作を提供する。他の機能(カードめくり、コンボ)とスムーズに連携し、競合を防ぐ。
手順:
- 告知(5n-1回試行):`selectShufflePair`関数で、未マッチのカードから2枚をランダムに選択。`shuffle-highlight`クラスを追加し、紫枠と点滅で告知。`shufflePair`配列に保存。
- 交換(5n回試行):`executeShuffle`関数で、告知済みの2枚を確認。マッチ済みならスキップ。仮要素でスライドアニメーション(0.5秒)を実行、DOMで単一スワップ、表/裏状態を保持。「SHUFFLE!」オーバーレイと600Hz三角波を再生。
- 試行数監視:`checkMatch`内で試行数(`attempts`)をチェック、5n-1回で告知、5n回で交換をトリガー。
- 状態管理:`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`で機能間の競合を防ぐ設計を学ぶ。