Repository: Showichiro/puzzle-poc
Pull request #22: feat
Author: login: s-uchihori
Created: 2025-05-01T16:12:44Z | Merged: 2025-05-01T16:49:58Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a card mechanic allowing players to spend 3 moves to draw a random score multiplier card, applying a multiplier between 0.1x and 1000x for 3 turns.
  - Added UI elements to display the current card effect and remaining turns, plus a button to activate the multiplier card.
- **Enhancements**
  - Improved bonus turns calculation for stage clear with diminishing returns based on excess score.
  - Streamlined game phase management using a unified game state for clearer gameplay flow.
- **Documentation**
  - Updated in-game descriptions to explain the multiplier card feature and revised bonus turn mechanics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/components/GameBoard.tsx b/src/components/GameBoard.tsx
index b05eeaa..02c3e18 100644
--- a/src/components/GameBoard.tsx
+++ b/src/components/GameBoard.tsx
@@ -25,7 +25,6 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
     selectedCell,
     setSelectedCell,
     isProcessing,
-    isGameOver,
     score,
     scoreMultiplier,
     resetBoard,
@@ -34,20 +33,24 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
     stage,
     currentMaxMoves,
     currentTargetScore,
-    isStageClear,
-    // ★ ステージクリアモーダル関連の state と関数を取得
-    showStageClearModal,
     handleProceedToNextStage,
-    showDifficultySelector, // 難易度選択フラグを追加
     handleDifficultySelected, // 難易度選択ハンドラを追加
+    gameState,
     nextStageGoals, // 次のステージの難易度ごとの目標を追加
     // ★ 現在の難易度を取得
     bonusMoves, // ★ bonusMoves を取得
+    // ★ カード関連の state と関数を取得
+    drawCard,
+    cardMultiplier,
+    cardTurnsLeft,
+    setCardMultiplier, // ターン経過で使用
+    setCardTurnsLeft, // ターン経過で使用
+    setScoreMultiplier,
   } = useGameBoard(initialDifficulty); // initialDifficulty をフックに渡す
 
   // セルクリック時のハンドラ
   const handleClick = (row: number, col: number) => {
-    if (isProcessing || isGameOver) return; // 処理中またはゲームオーバー時は操作不可
+    if (isProcessing || gameState === "gameOver") return; // 処理中またはゲームオーバー時は操作不可
 
     if (selectedCell) {
       const { row: selectedRow, col: selectedCol } = selectedCell;
@@ -69,6 +72,20 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
         const nextMoves = moves + 1;
         setMoves(nextMoves);
 
+        // ★ カード効果のターン経過処理
+        setCardTurnsLeft((prev) => {
+          if (prev <= 0) return prev; // nothing to do
+          const next = prev - 1;
+          if (next === 0) {
+            setCardMultiplier(1); // 効果終了
+            setScoreMultiplier(1);
+            console.log("Card effect ended.");
+          } else {
+            console.log(`Card effect: ${cardMultiplier}x, Turns left: ${next}`);
+          }
+          return next;
+        });
+
         // 入れ替えによってマッチが発生するかチェックし、連鎖処理を開始
         const initialMatches = findMatches(newBoard);
         if (initialMatches.length > 0) {
@@ -102,7 +119,7 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
     <div className="relative w-full">
       {/* ポップアップ表示のために relative を追加 */}
       <AnimatePresence>
-        {!showStageClearModal && isGameOver && (
+        {gameState === "gameOver" && (
           <GameOverModal
             resetBoard={resetBoard}
             stage={stage}
@@ -110,7 +127,7 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
           />
         )}
         {/* ★ ステージクリアモーダルを表示し、追加情報を渡す */}
-        {showStageClearModal && (
+        {gameState === "stageClear" && (
           <StageClearModal
             stage={stage}
             score={score} // スコアを渡す
@@ -120,7 +137,7 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
           />
         )}
         {/* ★ 難易度選択モーダルを表示 */}
-        {showDifficultySelector && nextStageGoals && (
+        {gameState === "difficultySelect" && nextStageGoals && (
           <DifficultySelector
             onSelectDifficulty={handleDifficultySelected}
             // ★ nextStageGoals の型を修正
@@ -137,17 +154,19 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
         scoreMultiplier={scoreMultiplier}
         stage={stage}
         moves={moves}
+        // ★ カード関連の props を渡す
+        cardMultiplier={cardMultiplier}
+        cardTurnsLeft={cardTurnsLeft}
+        drawCard={drawCard}
       />
 
       {/* --- ゲーム盤エリア --- */}
       {/* ★ ステージクリアモーダル表示中もゲーム盤を非表示 */}
-      {!showDifficultySelector && !showStageClearModal && (
+      {gameState === "playing" && (
         <div
           className={`grid gap-0 ${
             // ★ ステージクリアモーダル表示中も opacity を適用
-            isGameOver || isStageClear || showStageClearModal
-              ? "opacity-50"
-              : ""}`}
+            gameState !== "playing" ? "opacity-50" : ""}`}
           style={{
             gridTemplateColumns: `repeat(${BOARD_SIZE}, minmax(0, 1fr))`,
           }}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 501b1a0..cbaeb45 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -98,11 +98,19 @@ const Header: React.FC<HeaderProps> = ({ onOpenHistoryModal }) => {
             </p>
             <p>ボーナス手数:</p>
             <p>
-              目標スコアを上回ると、ボーナス手数が加算されます。 (最大3手)
+              目標スコアを上回ると、超過スコアに応じてボーナス手数が加算されます。超過スコアが大きいほど、ボーナス手数の増加率は緩やかになります。
             </p>
-            <ul>
+            <p className="mt-4 font-semibold">倍率変更機能:</p>
+            <ul className="list-disc list-inside ml-4">
+              <li>
+                「倍率変更」ボタンで3手数を消費して、特殊なカードを引けます。
+              </li>
+              <li>
+                3ターンの間、スコア計算時の倍率をランダムに変化させます
+                (0.1倍から1000倍)。
+              </li>
               <li>
-                計算式: ((スコア / 目標スコア - 1.0) * 100) / 500 (最大3手)
+                現在のカード効果（倍率と残りターン数）は、ゲーム画面上部の情報エリアで確認できます。
               </li>
             </ul>
             <button
diff --git a/src/components/InfoArea.tsx b/src/components/InfoArea.tsx
index a14b114..918ea22 100644
--- a/src/components/InfoArea.tsx
+++ b/src/components/InfoArea.tsx
@@ -8,6 +8,10 @@ interface InfoAreaProps {
   currentTargetScore: number;
   score: number;
   scoreMultiplier: number;
+  // ★ カード関連の props を追加
+  cardMultiplier: number;
+  cardTurnsLeft: number;
+  drawCard: () => void;
 }
 
 export const InfoArea: FC<InfoAreaProps> = (
@@ -18,11 +22,18 @@ export const InfoArea: FC<InfoAreaProps> = (
     moves,
     score,
     scoreMultiplier,
+    // ★ カード関連の props を受け取る
+    cardMultiplier,
+    cardTurnsLeft,
+    drawCard,
   },
 ) => {
   const { highestStage } = useHighestScore();
+  const canDrawCard = currentMaxMoves - moves >= 3; // カードを引けるかどうかのフラグ
+
   return (
-    <div className="grid grid-cols-3 gap-x-4 gap-y-1 mb-4 p-2 bg-gray-100 rounded">
+    // ★ grid-rows-3 に変更してカード情報を追加
+    <div className="grid grid-cols-3 grid-rows-3 gap-x-4 gap-y-1 mb-4 p-2 bg-gray-100 rounded">
       {/* Row 1 */}
       <div className="text-center">
         <span className="text-xs text-gray-600 block">Stage</span>
@@ -53,9 +64,34 @@ export const InfoArea: FC<InfoAreaProps> = (
       <div className="text-center">
         <span className="text-xs text-gray-600 block">倍率</span>
         <span className="text-lg font-semibold">
-          x{scoreMultiplier.toFixed(1)}
+          {/* ★ 小数点以下2桁まで表示 */}
+          x{scoreMultiplier.toFixed(2)}
         </span>
       </div>
+      {/* Row 3: Card Info and Draw Button */}
+      <div className="text-center col-span-2">
+        {cardTurnsLeft > 0 && ( // カード効果がある場合のみ表示
+          <>
+            <span className="text-xs text-gray-600 block">カード効果</span>
+            <span className="text-lg font-semibold text-blue-600">
+              スコア x{cardMultiplier.toFixed(2)} (あと{cardTurnsLeft}手)
+            </span>
+          </>
+        )}
+      </div>
+      <div className="text-center">
+        <button
+          onClick={drawCard}
+          disabled={!canDrawCard} // 手数が足りない場合は無効化
+          className={`px-2 py-1 text-sm rounded ${
+            canDrawCard
+              ? "bg-green-500 hover:bg-green-600 text-white"
+              : "bg-gray-300 text-gray-500 cursor-not-allowed"
+          }`}
+        >
+          倍率変更
+        </button>
+      </div>
     </div>
   );
 };
diff --git a/src/hooks/useGameBoard.ts b/src/hooks/useGameBoard.ts
index d82842a..6a5f010 100644
--- a/src/hooks/useGameBoard.ts
+++ b/src/hooks/useGameBoard.ts
@@ -14,6 +14,12 @@ import { useHighestScore } from "../contexts/HighestScoreContext";
 // Difficulty 型を定義
 type Difficulty = "easy" | "medium" | "hard";
 
+// ゲームの状態を定義
+type GameState =
+  | "playing"
+  | "stageClear"
+  | "difficultySelect"
+  | "gameOver";
 // ★ ステージ1の初期手数を定義
 const initialMovesMap: Record<Difficulty, number> = {
   easy: 5,
@@ -40,9 +46,9 @@ const calculateStageGoals = (
 
   // 難易度に応じたステージクリア時の加算手数
   const addedMovesMap: Record<Difficulty, number> = {
-    easy: 1, // 簡単なら多く加算
-    medium: 3,
-    hard: 5, // 難しいなら少なく加算
+    easy: 1,
+    medium: 2,
+    hard: 4,
   };
 
   // ★ 加算手数はステージや難易度で固定とする (シンプル化のため)
@@ -105,8 +111,6 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   >(null);
   // 操作中フラグ(連鎖中の誤操作防止)
   const [isProcessing, setIsProcessing] = useState(false);
-  // ゲームオーバーフラグ
-  const [isGameOver, setIsGameOver] = useState(false);
   // スコア
   const [score, setScore] = useState(0);
   // ステージレベル
@@ -122,12 +126,9 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   const [currentTargetScore, setCurrentTargetScore] = useState(
     initialTargetScore,
   ); // ★ Stage 1 の目標スコア
-  // ステージクリアフラグ
-  const [isStageClear, setIsStageClear] = useState(false);
-  // ★ ステージクリアモーダル表示フラグを追加
-  const [showStageClearModal, setShowStageClearModal] = useState(false);
-  // 難易度選択モーダル表示フラグ
-  const [showDifficultySelector, setShowDifficultySelector] = useState(false);
+  // ゲームの状態を管理する state
+  const [gameState, setGameState] = useState<GameState>("playing");
+
   // ★ 次のステージの難易度ごとの目標 (加算手数と目標スコア)
   const [nextStageGoals, setNextStageGoals] = useState<
     Record<
@@ -152,22 +153,20 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   >([]);
   const scoreIdCounter = useRef(0); // floating score にユニークIDを付与するためのカウンター
   const { setHighestStage } = useHighestScore();
+  // カード効果の状態
+  const [cardMultiplier, setCardMultiplier] = useState(1); // カードによるスコア倍率 (初期値1)
+  const [cardTurnsLeft, setCardTurnsLeft] = useState(0); // カード効果の残りターン数 (初期値0)
 
   // ステージクリアをチェックする関数
   const checkStageClear = (currentScore: number) => {
     // ステージクリア済み、ゲームオーバー、難易度選択中、ステージクリアモーダル表示中はチェックしない
-    if (
-      isStageClear ||
-      isGameOver ||
-      showDifficultySelector ||
-      showStageClearModal
-    ) {
+    if (gameState !== "playing") {
       return;
     }
 
     if (currentScore >= currentTargetScore) {
       // ステージクリア！
-      setIsStageClear(true);
+      setGameState("stageClear");
       console.log(`Stage ${stage} Clear! Score: ${currentScore}`);
 
       // ボーナス手数を計算
@@ -176,15 +175,15 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
         : 0;
       let calculatedBonusMoves = 0;
       if (scoreRatio > 1.0) {
-        calculatedBonusMoves = Math.floor(((scoreRatio - 1.0) * 100) / 500);
+        // ★ 超過スコアの割合に応じて逓減する計算式に変更 (例: 平方根に比例)
+        const excessRatio = scoreRatio - 1.0; // 超過した割合 (例: スコアが目標の1.5倍なら 0.5)
+        // 計算式例: (超過割合)^0.5 * 10
+        calculatedBonusMoves = Math.floor(Math.pow(excessRatio, 0.1) * 3);
       }
-      setBonusMoves(
-        calculatedBonusMoves > 0
-          ? Math.min(calculatedBonusMoves, 3)
-          : calculatedBonusMoves,
-      ); // 3手より大きなボーナスなし
+      // ★ 上限を撤廃
+      setBonusMoves(calculatedBonusMoves);
       // ★ 難易度選択ではなく、ステージクリアモーダルを表示
-      setShowStageClearModal(true);
+      setGameState("stageClear");
       // 次のステージの目標計算はここで行う
       const nextStage = stage + 1;
       // ★ 次のステージの目標計算 (calculateStageGoals は difficulty が必須になった)
@@ -200,36 +199,28 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   // ゲームオーバーをチェックする関数
   const checkGameOver = (currentMoves: number, currentScore: number) => {
     // ステージクリア済み、ゲームオーバー、難易度選択中、ステージクリアモーダル表示中はチェックしない
-    if (
-      isStageClear ||
-      isGameOver ||
-      showDifficultySelector ||
-      showStageClearModal
-    ) {
+    if (gameState !== "playing") {
       return;
     }
-
     // ステージクリアしておらず、かつ手数が上限に達していたらゲームオーバー判定
-    if (!isStageClear && currentMoves >= currentMaxMoves) {
+    if (currentMoves >= currentMaxMoves) {
       console.log(
         `Game Over - Stage ${stage}. Moves: ${currentMoves}, Score: ${currentScore}, Target: ${currentTargetScore}`,
       );
       saveGameHistory(stage);
-      setIsGameOver(true);
+      setGameState("gameOver");
     }
   };
 
   // ★ ステージクリアモーダルから難易度選択へ進む関数
   const handleProceedToNextStage = () => {
-    setShowStageClearModal(false); // ステージクリアモーダルを非表示
-    setShowDifficultySelector(true); // 難易度選択モーダルを表示
+    setGameState("difficultySelect"); // 難易度選択モーダルを表示
   };
 
   // 難易度選択後に次のステージに進む関数
   const handleDifficultySelected = (selectedDifficulty: Difficulty) => {
     // モーダル非表示と目標情報のリセット
-    setShowStageClearModal(false);
-    setShowDifficultySelector(false);
+    setGameState("playing");
     setNextStageGoals(null);
 
     // 次のステージの色を選択し、ステージ番号を更新
@@ -255,11 +246,8 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
     // 各種stateのリセット
     setMoves(0);
     setScore(0); // スコアはリセット
-    setIsStageClear(false);
     setBonusMoves(0);
-    setShowStageClearModal(false);
     setNextStageGoals(null); // 目標情報もリセット
-
     // 新しい盤面の生成と設定
     const newBoard = createAndInitializeBoard();
     setBoard(newBoard);
@@ -286,15 +274,63 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
 
     setMoves(0);
     setScore(0);
-    setIsGameOver(false);
-    setIsStageClear(false);
-    setShowStageClearModal(false);
-    setShowDifficultySelector(false);
+    setGameState("playing");
     setNextStageGoals(null);
     setBonusMoves(0);
     setScoreMultiplier(1);
     setFloatingScores([]);
     scoreIdCounter.current = 0;
+    // カード効果もリセット
+    setCardMultiplier(1);
+    setCardTurnsLeft(0);
+  };
+
+  // ★ カードを引く関数
+  const drawCard = () => {
+    console.log("drawCard function called."); // ★ デバッグログ追加
+    // 手数が3未満、処理中、ゲームオーバー、ステージクリア中などは引けない
+    const remainingMoves = currentMaxMoves - moves; // ★ 残り手数を計算
+    console.log(
+      `Checking conditions: remainingMoves=${remainingMoves}, isProcessing=${isProcessing}, gameState=${gameState}`,
+    ); // ★ デバッグログ追加
+    if (
+      remainingMoves < 3 ||
+      isProcessing ||
+      gameState !== "playing"
+    ) {
+      console.log("Cannot draw card now. Condition not met."); // ★ デバッグログ追加
+      return;
+    }
+
+    // 手数を3消費
+    const newMoves = moves + 3; // 手数は加算されるので注意
+    console.log(
+      `Drawing card: Consuming 3 moves. Old moves: ${moves}, New moves: ${newMoves}`,
+    ); // ★ 既存ログ (前の適用で入っていた)
+    setMoves(newMoves);
+
+    // ランダムな倍率を生成 (0.01 ~ 1000)
+    // 対数スケールでランダム性を出すと極端な値が出やすくなる
+    // Math.random() は 0以上1未満の値を返す
+    // ★ 最小値を 0.1 に変更
+    const randomLog = Math.random() * (Math.log(1000) - Math.log(0.1)) +
+      Math.log(0.1);
+    const randomMultiplier = Math.exp(randomLog);
+    const newMultiplier = parseFloat(randomMultiplier.toFixed(2)); // 小数点以下2桁に丸める
+
+    // カード効果を設定
+    setCardMultiplier(newMultiplier);
+    setCardTurnsLeft(3); // 効果は3ターン
+    setScoreMultiplier(newMultiplier);
+
+    console.log(
+      `Card drawn! Multiplier: ${newMultiplier}x for 3 turns. Moves left: ${
+        currentMaxMoves - newMoves
+      }`,
+    ); // ★ 既存ログ
+
+    // 手数消費によるゲームオーバーチェック
+    checkGameOver(newMoves, score);
   };
 
   // 連鎖の各ステップ（マッチ消去、落下、補充）を処理するヘルパー関数
@@ -344,7 +380,7 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
     const clearedBlocksBonus = Math.pow(matches.length, 1.5);
 
     const pointsEarned = Math.floor(
-      basePoints * clearedBlocksBonus * chainBonus, // blockTypeBonus は常に1なので省略
+      basePoints * clearedBlocksBonus * chainBonus * cardMultiplier, // ★ カード倍率を適用
     ); // 整数にする
 
     // スコアの state を更新
@@ -352,7 +388,8 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
     setScore(newChainScore);
 
     // スコア倍率の state を更新 (表示用)
-    setScoreMultiplier(chainBonus); // 表示用には連鎖ボーナスのみを使用
+    // ★ 連鎖ボーナスとカード倍率を掛け合わせた値を設定
+    setScoreMultiplier(chainBonus * cardMultiplier);
 
     // 加算スコア表示用の情報を生成
     const currentFloatingScores = matches.map(({ row, col }) => ({
@@ -381,12 +418,7 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   ) => {
     if (isProcessing) return;
     // ゲームオーバー or ステージクリア or 難易度選択中 or ステージクリアモーダル表示中なら処理しない
-    if (
-      isGameOver ||
-      isStageClear ||
-      showDifficultySelector ||
-      showStageClearModal
-    ) {
+    if (gameState !== "playing") {
       setIsProcessing(false);
       return;
     }
@@ -416,20 +448,21 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
       matches = findMatches(boardAfterProcessing);
 
       // ループの最後にステージクリア状態をチェック (連鎖中にクリアした場合にループを抜ける)
-      if (isStageClear) {
+      if (gameState !== "playing") {
         console.log("Stage cleared during chain, exiting process loop.");
         break; // 連鎖処理を中断
       }
     }
 
+    // 連鎖が終わったら倍率をリセットする
+    // ★ カード効果が残っている場合はカード倍率に、なければ1にリセット
+    setScoreMultiplier(cardTurnsLeft > 0 ? cardMultiplier : 1);
+
     // 連鎖処理完了後に最終的なスコアでゲームステータスをチェック
-    if (!isStageClear && !showDifficultySelector && !showStageClearModal) {
+    if (gameState === "playing") {
       // ステージクリアしていなければ判定
       checkStageClear(currentChainScore);
     }
-    // 連鎖が終わったら倍率をリセットするのは継続
-    setScoreMultiplier(1);
-
     // 連鎖処理完了後に最終的なスコアと手数でゲームオーバーをチェック
     checkGameOver(currentMoves, currentChainScore);
 
@@ -452,7 +485,6 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
     selectedCell,
     setSelectedCell,
     isProcessing,
-    isGameOver,
     score,
     scoreMultiplier,
     resetBoard,
@@ -461,13 +493,18 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
     stage,
     currentMaxMoves,
     currentTargetScore,
-    isStageClear,
-    showStageClearModal, // 追加
     handleProceedToNextStage, // 追加
-    showDifficultySelector, // 難易度選択フラグを追加
     handleDifficultySelected, // 難易度選択ハンドラを追加
     nextStageGoals, // 次のステージの難易度ごとの目標を追加
     bonusMoves, // ★ ボーナス手数を追加
+    drawCard, // ★ カードを引く関数を追加
+    cardMultiplier, // ★ カード倍率を追加
+    cardTurnsLeft, // ★ カード残りターン数を追加
+    setCardMultiplier, // ★ カード倍率セッターを追加
+    setCardTurnsLeft, // ★ カード残りターン数セッターを追加
+    setScoreMultiplier,
+    gameState, // ゲームの状態を追加
+    // handleMoveAction, // ★ ターン経過処理を含む関数 (UI側で呼び出す想定)
   };
 };
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/hooks/useGameBoard.ts (1)</summary><blockquote>

`167-188`: _🛠️ Refactor suggestion_

**Duplicate `setGameState("stageClear")` triggers unnecessary extra render**

`gameState` is set to `"stageClear"` on line 169 and then again on line 186.  
The second call is redundant and costs an extra state-update/render cycle.

```diff
-      setGameState("stageClear");
       // ボーナス手数を計算
```

Removing the duplicate keeps behaviour identical while avoiding an avoidable render.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/components/Header.tsx (1)</summary><blockquote>

`103-115`: **Unify terminology & tweak list markup**

Mixing 「手数」 and 「ターン」 may confuse players—both words represent “moves”.  
While you’re tweaking text, consider keeping each `<li>` on one line to avoid unintended spacing in some browsers.

```diff
- 「倍率変更」ボタンで3手数を消費して、特殊なカードを引けます。
+ 「倍率変更」ボタンで3ターン分の手数を消費して、特殊なカードを引けます。
```

</blockquote></details>
<details>
<summary>src/components/GameBoard.tsx (1)</summary><blockquote>

`166-171`: **Redundant opacity condition**

Because this block is rendered only when `gameState === "playing"`,  
`gameState !== "playing"` is always false inside the template literal.

```diff
-${gameState !== "playing" ? "opacity-50" : ""}
+""
```

Removing the dead branch simplifies future maintenance.

</blockquote></details>
<details>
<summary>src/components/InfoArea.tsx (1)</summary><blockquote>

`83-94`: **Add an ARIA label for better accessibility**

Screen-reader users won’t know what “倍率変更” means in context.  
A short `aria-label` clarifies intent without altering visuals.

```diff
-<button
-  onClick={drawCard}
+<button
+  aria-label="Draw multiplier-change card (costs 3 moves)"
+  onClick={drawCard}
```

</blockquote></details>
<details>
<summary>src/hooks/useGameBoard.ts (2)</summary><blockquote>

`291-336`: **Verbose `console.log` statements will flood production logs**

`drawCard` contains several debug `console.log` calls. These are helpful during development but will:

1. Pollute the browser console in production.
2. Slightly degrade performance on lower-end devices.

Wrap them behind a development flag or remove them before release.

```diff
-    console.log("drawCard function called.");
...
-    console.log(
-      `Checking conditions: remainingMoves=${remainingMoves}, isProcessing=${isProcessing}, gameState=${gameState}`,
-    );
...
-    console.log(
-      `Drawing card: Consuming 3 moves. Old moves: ${moves}, New moves: ${newMoves}`,
-    );
...
-    console.log(
-      `Card drawn! Multiplier: ${newMultiplier}x for 3 turns. Moves left: ${
-        currentMaxMoves - newMoves
-      }`,
-    );
```

---

`17-22`: **Consider promoting `GameState` to a TypeScript `enum` for type-safety**

String unions work, but using an `enum`:

```ts
export enum GameState {
  Playing = "playing",
  StageClear = "stageClear",
  DifficultySelect = "difficultySelect",
  GameOver = "gameOver",
}
```

gives:

- Autocomplete with a shared namespace (`GameState.Playing` etc.).
- Protection against misspelling raw strings.
- Slight bundle-size reduction after minification.

This is optional but improves robustness at minimal cost.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 952834f0a27db872f5813273407287e8549d8682 and 63801904433343663c72238488d185327fb2329d.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/components/GameBoard.tsx` (5 hunks)
* `src/components/Header.tsx` (1 hunks)
* `src/components/InfoArea.tsx` (3 hunks)
* `src/hooks/useGameBoard.ts` (11 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/components/InfoArea.tsx (1)</summary><blockquote>

<details>
<summary>src/contexts/HighestScoreContext.tsx (1)</summary>

* `useHighestScore` (37-45)

</details>

</blockquote></details>
<details>
<summary>src/hooks/useGameBoard.ts (1)</summary><blockquote>

<details>
<summary>src/utils/saveGameHistory.ts (1)</summary>

* `saveGameHistory` (1-28)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/components/Header.tsx (1)</summary>

`101-102`: **Clearer bonus-turn explanation looks good**

The new wording better captures the diminishing-returns formula without exposing fragile implementation details. 👍

</details>
<details>
<summary>src/components/GameBoard.tsx (1)</summary>

`53-54`: **Guard condition aligns with new state machine**

Nice catch replacing the old boolean flag with `gameState === "gameOver"`.  
This keeps the UI consistent with the centralised FSM introduced in the hook.

</details>
<details>
<summary>src/components/InfoArea.tsx (1)</summary>

`32-33`: **`canDrawCard` calculation is clear and self-documenting**

Good use of a derived flag instead of hard-coding the logic in JSX.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T16:15:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/hooks/useGameBoard.ts (2)</summary><blockquote>

`275-286`: **Multiple duplicated state-updates inside `resetBoard`**

Within `resetBoard` you call the same setters twice (`setGameState`, `setScoreMultiplier`).  
Besides being noisy, each extra call schedules an additional React render.

```diff
-    setGameState("playing");
...
-    setScoreMultiplier(1);
...
-    setGameState("playing");
+    setGameState("playing");         // keep a single invocation
...
+    setScoreMultiplier(1);           // keep a single invocation
```

Consider grouping all state resets in one contiguous block to keep the function concise and render-efficient.

---

`157-159`: _⚠️ Potential issue_

**Card effect never expires – `cardTurnsLeft` is never decremented**

`cardMultiplier` is applied when scoring (line 383) and the UI multiplier is reset based on  
`cardTurnsLeft` (line 459), yet the counter is never decremented anywhere in the hook.  
Unless some outer component mutates it, the multiplier will stay active indefinitely.



Recommended fix (one possible spot – after a *successful* move):  

```diff
// at the end of a player move, before exiting processMatchesAndGravity
if (cardTurnsLeft > 0) {
  setCardTurnsLeft((t) => t - 1);
  if (cardTurnsLeft === 1) {
    // effect just ended -> restore default multiplier
    setCardMultiplier(1);
    setScoreMultiplier(1);
  }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/hooks/useGameBoard.ts (1)</summary><blockquote>

`288-334`: **Card mechanic implementation looks solid**

The `drawCard` function correctly:
- Checks if the player has enough moves (at least 3) and if the game is in the playing state
- Consumes moves as a cost for drawing a card
- Generates a random multiplier on a logarithmic scale
- Sets up the card effect duration
- Checks for game over after consuming moves

However, consider adding safeguards around the multiplier range. Values near 1000x could make the game too easy, while values near 0.1x could frustrate players.



```diff
// Add min/max caps to prevent extreme values
- const newMultiplier = parseFloat(randomMultiplier.toFixed(2)); // 小数点以下2桁に丸める
+ let newMultiplier = parseFloat(randomMultiplier.toFixed(2)); // 小数点以下2桁に丸める
+ // Cap multiplier between 0.2 and 100 to prevent extreme gameplay effects
+ newMultiplier = Math.max(0.2, Math.min(100, newMultiplier));
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bcaea8ff1383afca9a76d9cf6a2986b457e7a56e and a84edbe7243a01bf099c4b114031011faa081075.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/components/GameBoard.tsx` (5 hunks)
* `src/hooks/useGameBoard.ts` (11 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/components/GameBoard.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/hooks/useGameBoard.ts (1)</summary><blockquote>

<details>
<summary>src/utils/saveGameHistory.ts (1)</summary>

* `saveGameHistory` (1-28)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/hooks/useGameBoard.ts (5)</summary>

`17-22`: **Great refactoring with unified GameState type!**

Replacing multiple boolean flags (`isGameOver`, `isStageClear`, etc.) with a single `gameState` enum makes the code more maintainable and less prone to state inconsistencies. This is a good application of the State pattern.



Also applies to: 129-131

---

`179-184`: **Improved bonus move calculation formula provides better scaling**

The new formula using a fractional exponent (`excessRatio^0.1 * 3`) creates a more balanced curve for bonus moves compared to a linear formula. Removing the upper limit allows for exceptional performances to be properly rewarded.

---

`382-393`: **Correctly implemented card multiplier in score calculation**

The card multiplier is properly applied to the score calculation formula, and the displayed score multiplier correctly reflects both the chain bonus and card effect.

---

`457-460`: **Good reset logic for score multiplier after chain completion**

After chain processing completes, the score multiplier is correctly reset to either the card multiplier (if effect is active) or to 1 (if no effect).

---

`500-507`: **Good API design exposing card-related functionality**

The hook correctly exposes the new card-related state, setters, and the `drawCard` function, making them available to UI components. The unified `gameState` is also exposed, replacing the previous boolean flags.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T16:52:27Z

Review comments:
diff_hunk: @@ -69,6 +72,21 @@ const GameBoard: React.FC<GameBoardProps> = ({ initialDifficulty }) => { // Prop
         const nextMoves = moves + 1;
         setMoves(nextMoves);
 
+        // ★ カード効果のターン経過処理
+        if (cardTurnsLeft > 0) {
+          const newTurnsLeft = cardTurnsLeft - 1;
+          setCardTurnsLeft(newTurnsLeft);
+          if (newTurnsLeft === 0) {
+            setCardMultiplier(1); // 効果終了
+            setScoreMultiplier(1);
+            console.log("Card effect ended.");
+          } else {
+            console.log(
+              `Card effect: ${cardMultiplier}x, Turns left: ${newTurnsLeft}`,
+            );
+          }
+        }
path: src/components/GameBoard.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use functional `setState` to avoid stale-closure bugs**

`cardTurnsLeft` and `cardMultiplier` come from the render in which the click occurred.  
If a second click somehow fires before React finishes updating state (rare, but possible on very fast devices), the decrement could be based on an old value.

```diff
-if (cardTurnsLeft > 0) {
-  const newTurnsLeft = cardTurnsLeft - 1;
-  setCardTurnsLeft(newTurnsLeft);
-  if (newTurnsLeft === 0) {
+setCardTurnsLeft(prev => {
+  if (prev <= 0) return prev;        // nothing to do
+  const next = prev - 1;
+  if (next === 0) {
     setCardMultiplier(1);
     setScoreMultiplier(1);
     console.log("Card effect ended.");
-  } else {
-    console.log(`Card effect: ${cardMultiplier}x, Turns left: ${newTurnsLeft}`);
-  }
-}
+  } else {
+    console.log(`Card effect: ${cardMultiplier}x, Turns left: ${next}`);
+  }
+  return next;
+});
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // ★ カード効果のターン経過処理
        setCardTurnsLeft(prev => {
          if (prev <= 0) return prev;        // nothing to do
          const next = prev - 1;
          if (next === 0) {
            setCardMultiplier(1); // 効果終了
            setScoreMultiplier(1);
            console.log("Card effect ended.");
          } else {
            console.log(`Card effect: ${cardMultiplier}x, Turns left: ${next}`);
          }
          return next;
        });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T16:15:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,22 +153,20 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
   >([]);
   const scoreIdCounter = useRef(0); // floating score にユニークIDを付与するためのカウンター
   const { setHighestStage } = useHighestScore();
+  // カード効果の状態
+  const [cardMultiplier, setCardMultiplier] = useState(1); // カードによるスコア倍率 (初期値1)
+  const [cardTurnsLeft, setCardTurnsLeft] = useState(0); // カード効果の残りターン数 (初期値0)
 
path: src/hooks/useGameBoard.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Card effect never expires – `cardTurnsLeft` is never decremented**

`cardMultiplier` is applied when scoring (line 385) and the UI multiplier is reset based on  
`cardTurnsLeft` (line 461), yet the counter is never decremented anywhere in the hook.  
Unless some outer component mutates it, the multiplier will stay active indefinitely.

Recommended fix (one possible spot – after a *successful* move):  

```diff
// at the end of a player move, before exiting processMatchesAndGravity
if (cardTurnsLeft > 0) {
  setCardTurnsLeft((t) => t - 1);
  if (cardTurnsLeft === 1) {
    // effect just ended -> restore default multiplier
    setCardMultiplier(1);
    setScoreMultiplier(1);
  }
}
```

Alternatively, expose and manage a dedicated `advanceTurn()` helper so the decrement logic lives in one place.



Also applies to: 461-462

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T16:15:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,15 +274,65 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
 
     setMoves(0);
     setScore(0);
-    setIsGameOver(false);
-    setIsStageClear(false);
-    setShowStageClearModal(false);
-    setShowDifficultySelector(false);
+    setGameState("playing");
     setNextStageGoals(null);
     setBonusMoves(0);
     setScoreMultiplier(1);
     setFloatingScores([]);
     scoreIdCounter.current = 0;
+    // カード効果もリセット
+    setCardMultiplier(1);
+    setCardTurnsLeft(0);
+    setScoreMultiplier(1);
+    setGameState("playing");
+  };
path: src/hooks/useGameBoard.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Multiple duplicated state-updates inside `resetBoard`**

Within `resetBoard` you call the same setters twice (`setGameState`, `setScoreMultiplier`).  
Besides being noisy, each extra call schedules an additional React render.

```diff
-    setGameState("playing");
...
-    setScoreMultiplier(1);
...
-    setGameState("playing");
+    setGameState("playing");         // keep a single invocation
...
+    setScoreMultiplier(1);           // keep a single invocation
```

Consider grouping all state resets in one contiguous block to keep the function concise and render-efficient.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    setMoves(0);
    setScore(0);
    setGameState("playing");         // keep a single invocation
    setNextStageGoals(null);
    setBonusMoves(0);
    setScoreMultiplier(1);           // keep a single invocation
    setFloatingScores([]);
    scoreIdCounter.current = 0;
    // カード効果もリセット
    setCardMultiplier(1);
    setCardTurnsLeft(0);
  };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T16:15:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,15 +274,63 @@ const useGameBoard = (initialDifficulty: Difficulty) => {
 
     setMoves(0);
     setScore(0);
-    setIsGameOver(false);
-    setIsStageClear(false);
-    setShowStageClearModal(false);
-    setShowDifficultySelector(false);
+    setGameState("playing");
     setNextStageGoals(null);
     setBonusMoves(0);
     setScoreMultiplier(1);
     setFloatingScores([]);
     scoreIdCounter.current = 0;
+    // カード効果もリセット
+    setCardMultiplier(1);
+    setCardTurnsLeft(0);
+  };
+
+  // ★ カードを引く関数
+  const drawCard = () => {
+    console.log("drawCard function called."); // ★ デバッグログ追加
+    // 手数が3未満、処理中、ゲームオーバー、ステージクリア中などは引けない
+    const remainingMoves = currentMaxMoves - moves; // ★ 残り手数を計算
+    console.log(
+      `Checking conditions: remainingMoves=${remainingMoves}, isProcessing=${isProcessing}, gameState=${gameState}`,
+    ); // ★ デバッグログ追加
+    if (
+      remainingMoves < 3 ||
+      isProcessing ||
+      gameState !== "playing"
+    ) {
+      console.log("Cannot draw card now. Condition not met."); // ★ デバッグログ追加
+      return;
+    }
+
+    // 手数を3消費
+    const newMoves = moves + 3; // 手数は加算されるので注意
+    console.log(
+      `Drawing card: Consuming 3 moves. Old moves: ${moves}, New moves: ${newMoves}`,
+    ); // ★ 既存ログ (前の適用で入っていた)
+    setMoves(newMoves);
+
+    // ランダムな倍率を生成 (0.01 ~ 1000)
+    // 対数スケールでランダム性を出すと極端な値が出やすくなる
+    // Math.random() は 0以上1未満の値を返す
+    // ★ 最小値を 0.1 に変更
+    const randomLog = Math.random() * (Math.log(1000) - Math.log(0.1)) +
+      Math.log(0.1);
+    const randomMultiplier = Math.exp(randomLog);
+    const newMultiplier = parseFloat(randomMultiplier.toFixed(2)); // 小数点以下2桁に丸める
+
path: src/hooks/useGameBoard.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider unit testing the card multiplier generation**

The logarithmic random multiplier generation is complex and critical to game balance. Add unit tests to verify:
1. The distribution of values
2. Range boundaries (min: 0.1, max: 1000)
3. Proper rounding to 2 decimal places

---

🏁 Script executed:

```shell
#!/bin/bash
# Test the distribution of card multiplier values
cat << EOF > test_multiplier.js
// Simulate multiplier generation 1000 times and output distribution stats
const results = [];
for (let i = 0; i < 1000; i++) {
  const randomLog = Math.random() * (Math.log(1000) - Math.log(0.1)) + Math.log(0.1);
  const randomMultiplier = Math.exp(randomLog);
  const multiplier = parseFloat(randomMultiplier.toFixed(2));
  results.push(multiplier);
}

// Basic stats
const min = Math.min(...results);
const max = Math.max(...results);
const avg = results.reduce((sum, val) => sum + val, 0) / results.length;

// Distribution buckets
const buckets = {
  "0.1-1": 0,
  "1-10": 0,
  "10-100": 0,
  "100-1000": 0,
  "1000+": 0
};

results.forEach(val => {
  if (val < 1) buckets["0.1-1"]++;
  else if (val < 10) buckets["1-10"]++;
  else if (val < 100) buckets["10-100"]++;
  else if (val <= 1000) buckets["100-1000"]++;
  else buckets["1000+"]++;
});

console.log("Min:", min);
console.log("Max:", max);
console.log("Avg:", avg.toFixed(2));
console.log("Distribution:");
for (const [range, count] of Object.entries(buckets)) {
  console.log(`${range}: ${count} (${(count/1000*100).toFixed(1)}%)`);
}
EOF

node test_multiplier.js
```

Length of output: 363

---

To avoid Bash interpolating your `${…}` placeholders, quote the here-doc delimiter and rerun:

---

🏁 Script executed:

```shell
#!/bin/bash
# Generate and run a fixed multiplier distribution test
cat << 'EOF' > test_multiplier.js
// Simulate multiplier generation 1000 times and output distribution stats
const results = [];
for (let i = 0; i < 1000; i++) {
  const randomLog = Math.random() * (Math.log(1000) - Math.log(0.1)) + Math.log(0.1);
  const randomMultiplier = Math.exp(randomLog);
  const multiplier = parseFloat(randomMultiplier.toFixed(2));
  results.push(multiplier);
}

const min = Math.min(...results);
const max = Math.max(...results);
const avg = results.reduce((sum, val) => sum + val, 0) / results.length;

const buckets = {
  "0.1-1": 0,
  "1-10": 0,
  "10-100": 0,
  "100-1000": 0,
  "1000+": 0
};

results.forEach(val => {
  if (val < 1) buckets["0.1-1"]++;
  else if (val < 10) buckets["1-10"]++;
  else if (val < 100) buckets["10-100"]++;
  else if (val <= 1000) buckets["100-1000"]++;
  else buckets["1000+"]++;
});

console.log("Min:", min);
console.log("Max:", max);
console.log("Avg:", avg.toFixed(2));
console.log("Distribution:");
for (const [range, count] of Object.entries(buckets)) {
  console.log(\`\${range}: \${count} (\${(count/1000*100).toFixed(1)}%)\`);
}
EOF

node test_multiplier.js
```

Length of output: 810

---



</details>

**Add unit tests for card multiplier generation**  
The log-scale random multiplier in `src/hooks/useGameBoard.ts` (≈lines 312–320) is critical for game balance. Please add automated tests to cover:  
- Range boundaries (min 0.1, max 1000)  
- Two-decimal rounding accuracy  
- Approximate log-uniform distribution across buckets (e.g. 0.1–1, 1–10, 10–100, 100–1000)

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T16:52:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-01T16:12:51Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce a unified `gameState` string to replace multiple boolean flags for tracking game phases, centralizing control flow in both the `GameBoard` component and the `useGameBoard` hook. A new card mechanic is implemented, allowing players to spend moves to draw a card that temporarily alters the score multiplier for a limited number of turns. The bonus move calculation formula is updated to use a fractional exponent, and related UI and documentation are revised to reflect the new mechanics. The `InfoArea` component is expanded to display card effects and provide a button for drawing cards.

## Changes

| File(s)                                    | Change Summary |
|--------------------------------------------|----------------|
| `src/components/GameBoard.tsx`             | Refactored to use a single `gameState` string for game phase control; integrated card mechanic state and handlers from `useGameBoard`; updated rendering logic for modals and game display based on `gameState`; passed card-related props to `InfoArea`. |
| `src/hooks/useGameBoard.ts`                 | Replaced multiple boolean flags with `gameState` enum; implemented card mechanic with state for multiplier and turns left; added `drawCard` function; updated bonus move calculation; adjusted score calculation to include card multiplier; exposed new states and functions in hook return. |
| `src/components/InfoArea.tsx`               | Extended props to include card mechanic state and handler; updated layout to a 3x3 grid; added display for card effect and a button to draw cards; improved score multiplier precision. |
| `src/components/Header.tsx`                 | Revised bonus turns explanation; added section describing the new multiplier card mechanic; updated documentation to match new game logic. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Player
    participant InfoArea
    participant GameBoard
    participant useGameBoard

    Player->>InfoArea: Clicks "倍率変更" (Change Multiplier) button
    InfoArea->>GameBoard: Calls drawCard()
    GameBoard->>useGameBoard: drawCard()
    useGameBoard->>useGameBoard: Check if moves >= 3 and gameState == "playing"
    useGameBoard->>useGameBoard: Generate random cardMultiplier (0.1x~1000x), set cardTurnsLeft=3
    useGameBoard->>useGameBoard: Deduct 3 moves, update scoreMultiplier
    useGameBoard-->>GameBoard: Updated cardMultiplier, cardTurnsLeft, moves
    GameBoard-->>InfoArea: Pass updated card-related props
    InfoArea->>Player: Display card effect and remaining turns
```

## Poem

> In the warren of code, a new card appears,  
> Multipliers wild, bring cheers (and some fears!).  
> With a hop and a tap, three moves disappear—  
> For a chance at a thousand, or a tenth, my dear!  
> Game states now tidy, the flow crystal clear,  
> Rabbits rejoice, for new features are here!  
> 🃏✨🐇

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Showichiro/puzzle-poc&utm_content=22):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-01T16:50:02Z
body: > [!CAUTION]
> No docstrings were generated.
author_association: CONTRIBUTOR