Skip to content

Guarantee solvable boards (+ order-strategy seam, explainer & difficulty review)#45

Merged
riethmayer merged 9 commits into
masterfrom
solvable-board-generation
May 25, 2026
Merged

Guarantee solvable boards (+ order-strategy seam, explainer & difficulty review)#45
riethmayer merged 9 commits into
masterfrom
solvable-board-generation

Conversation

@riethmayer

@riethmayer riethmayer commented May 24, 2026

Copy link
Copy Markdown
Owner

What & why

Randomly filling the turtle layout frequently dealt unsolvable boards, so whether a game could be won at all was partly luck — which undercuts a skill game. This makes every board solvable by construction, adds a strategy seam to tune board character/difficulty, and includes review docs.

Not for merge yet — opening for team review of the approach, the scatter default, and the difficulty direction.

Changes

  • Solvable-by-construction generator (src/utils/init-gameboard.ts): peel the fixed layout into a valid removal order, then lay matched token pairs onto it → every board is winnable for any shuffle.
  • Shared freeness rule (src/utils/board-rules.ts, isSelectable): single source of truth; the store's allowedforSelection now delegates to it so game and generator can't diverge.
  • Order-strategy port/adapter (src/utils/order-strategies.ts): swappable peel policies — topDownRandom, scatter (default), original, bottomUpRandom, plus a deterministic canonical fallback. Default is scatter because the layer-greedy policies pre-stack matching pairs at the top of the pyramid (100% of boards); scatter spreads matches across layers (0%).
  • Dev-only in-game switcher (/play): s / Shift+S cycle, r re-deal, hover for descriptions; hidden in production.
  • Docs: animated explainer (docs/solvable-boards.html + docs/gifs/) and a measured difficulty review (docs/strategy-difficulty-report.html).

Difficulty findings (for the team)

Branching factor = legal moves available per step (higher = easier):

strategy branching forced-move %
bottomUpRandom 4.5 (hardest) 30.0%
topDownRandom (old default) 7.5 3.8%
scatter (default) 9.6 3.4%
original 9.7 (easiest) 3.8%

scatter is ~28% more forgiving than the old default. The deliberate difficulty lever (symbol labeling / copy-spread) is currently unused — see the report for the recommendation (add a difficulty dial) and open questions for design.

Testing

  • yarn test101 pass (incl. re-solving generated boards across levels and every strategy).
  • yarn type-check and yarn lint clean (one pre-existing unrelated warning).

Notes for reviewers

  • docs/gifs/ adds ~4 MB of binaries (clips rendered from the real algorithms). Happy to move to Git LFS or drop from the repo if preferred.
  • Rolling the default back to the previous behaviour is a one-line DEFAULT_STRATEGY change (or an env / localStorage override) — topDownRandom is preserved verbatim.
  • No DB or schema changes.

Summary by CodeRabbit

  • New Features

    • Guaranteed solvable board generation and a pluggable order-strategy system (scatter default + others)
    • Dev strategy switcher UI with keyboard shortcuts (s / Shift+S to cycle, r to re-deal)
  • Bug Fixes / Improvements

    • Tile selection logic unified to a single selectable rule
    • Redeal action to regenerate the current level
  • Persistence / Backend

    • Strategy is now recorded with scores and included in game POSTs; DB migration added
  • Tests

    • New tests for order strategies and solvable-board generation
  • Documentation

    • Docs explaining board generation, order strategies, and developer override UX
  • UI

    • Highscores list now shows the strategy used under each player name

Review Change Stack

Randomly filling the turtle layout frequently produced unsolvable boards,
so whether a game could be won at all was partly luck. Build boards by
recording a valid solution instead of dealing and hoping.

- board-rules.ts: extract the geometric freeness rule (isSelectable) into a
  shared pure function, reused by the store's allowedforSelection so the
  game and generator can never disagree about what is reachable.
- init-gameboard.ts: peel the layout into a valid removal order, then lay
  matched token pairs onto it. Replaying the order wins, so every board is
  solvable by construction for any shuffle. Verified by re-solving deals.
- order-strategies.ts: ports & adapters seam for the peel-order policy
  (topDownRandom, scatter [default], original, bottomUpRandom; deterministic
  canonical fallback). scatter pairs the highest free tile with the lowest,
  spreading matches across layers to avoid pre-stacked matching pairs at the
  top of the pyramid. Selectable via env / localStorage / runtime override.
- game-store: allowedforSelection delegates to the shared rule; add
  redealCurrentLevel to re-deal the current level on demand.
Dev-only badge on /play to compare peel strategies live: s / Shift+S to
cycle, r to re-deal the same strategy, hover for descriptions. Hidden in
production builds; the selection persists via localStorage.
- solvable-boards.html: animated explainer contrasting random fill (often
  unsolvable) with solvability-by-construction; references docs/gifs.
- gifs/: short looping clips rendered from the real algorithms.
- strategy-difficulty-report.html: measured difficulty proxies (branching
  factor, forced-move rate, copy-spread) across strategies, for game-design
  review of the difficulty/look trade-off.
@vercel

vercel Bot commented May 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eureka Ready Ready Preview, Comment May 25, 2026 11:22am

@supabase

supabase Bot commented May 24, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project lthqyqlislwikgoxmttn because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@riethmayer, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 43 minutes and 17 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a11d0c5-5152-4565-b74d-005e79c08fca

📥 Commits

Reviewing files that changed from the base of the PR and between 59f40b5 and 24e7920.

📒 Files selected for processing (3)
  • src/components/leaderboard/index.tsx
  • src/utils/order-strategies.test.ts
  • src/zustand/game-store.test.ts
📝 Walkthrough

Walkthrough

Solvable board generation added via pluggable order strategies, geometric selectability rules, deterministic shuffling, store persistence of strategy, a dev-only StrategySwitcher UI with keyboard controls, tests for strategies and generation, UI/type/DB changes to persist/display strategy, and supporting documentation.

Changes

Solvability-by-Construction with Order Strategies

Layer / File(s) Summary
Tile selectability and shuffle utility
src/utils/board-rules.ts, src/utils/shuffle.ts
Adds isSelectable(board,index) using geometric freeness rules and shuffleInPlace (Fisher–Yates) with optional RNG injection.
Order strategy system: types and implementations
src/utils/order-strategies.ts
Introduces RemovalOrder, PairSelector, OrderStrategy, the shared peelWith loop, and strategy adapters: topDownRandom, bottomUpRandom, original, scatter, and deterministic canonical.
Strategy runtime controls & console API
src/utils/order-strategies.ts
Adds setOrderStrategy/getOrderStrategy with dev precedence (runtime → localStorage → env → default), listOrderStrategies, and window.eurekaOrder helpers.
Solvable board generation
src/utils/init-gameboard.ts
Implements dealSolvableBoard(level, random, strategy) that retries strategy.peel (up to 25 attempts), falls back to canonical, shuffles and assigns token pairs along the removal order, marks grace tiles, and refactors initializeGameBoard() to delegate to it.
Game store: strategy, selection, and re-deal
src/zustand/game-store.ts
Adds strategy to store state (initialized from current strategy), replaces inline selection logic with isSelectable(...), adds redealCurrentLevel() to regenerate the current level, and includes strategy in saved post-game payloads.
Dev-only UI: StrategySwitcher
src/components/strategy-switcher/index.tsx
Non-production React badge showing available strategies, highlights current, keyboard controls (s, Shift+S, r) and a cycle button; applies via setOrderStrategy() and redealCurrentLevel().
Play page integration
src/app/play/page.tsx
Imports and renders StrategySwitcher alongside existing UI on the /play page.
Order strategy tests
src/utils/order-strategies.test.ts
Vitest suite ensuring strategies produce solvable orders, determinism/variability checks, getOrderStrategy resolution, public catalogue contents, and strand statistics.
Solvable board generation tests
src/utils/solvable-board.test.ts
Vitest suite validating dealSolvableBoard correctness: token counts, solvability across RNGs/levels, variability, and grace-tile behavior.
UI, types, persistence, migration
src/app/highscores/page.tsx, src/db/select-game.ts, src/types/game.ts, src/utils/post-game-state.ts, supabase/migrations/20260525_add_strategy_to_games.sql
Displays and persists strategy in highscores, adds strategy to DB/types/post payload, and adds an idempotent DB migration to add games.strategy (NOT NULL default 'random').
Documentation
CLAUDE.md
Documents the solvability-by-construction approach, order-strategy seam, available strategies, fallback behavior, runtime override precedence, and dev UX (keyboard + console API).

Sequence Diagram

sequenceDiagram
  participant Dev as Developer (keyboard/console)
  participant UI as StrategySwitcher
  participant API as setOrderStrategy/getOrderStrategy
  participant Store as GameStore (redealCurrentLevel)
  participant Generator as dealSolvableBoard
  participant Strategy as OrderStrategy.peel()
  participant Fallback as canonical.peel()
  participant Shuffle as shuffleInPlace
  Dev->>UI: press 's' / 'Shift+S' / 'r' or call window.eurekaOrder
  UI->>API: setOrderStrategy(name)
  API->>API: persist runtime override/localStorage (dev)
  UI->>Store: redealCurrentLevel()
  Store->>Generator: initializeGameBoard(level, strategy)
  Generator->>Strategy: strategy.peel(random) attempts
  alt strategy returns null (strand)
    Generator->>Fallback: canonical.peel()
    Fallback-->>Generator: RemovalOrder
  else success
    Strategy-->>Generator: RemovalOrder
  end
  Generator->>Shuffle: shuffleInPlace(tokens, random)
  Shuffle-->>Generator: shuffled tokens
  Generator-->>Store: new GameBoard + solution
  Store-->>UI: updated board (animations)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • riethmayer/eureka#43: Overlaps on /highscores UI changes — this PR adds strategy display while #43 refactors the highscores page to use ISR and a Leaderboard client component.

Poem

I’m a rabbit who shuffles and peels by design,
I hop scatter layers then nudge them in line.
Press s or Shift+S, r to renew—
Dev badges glow, solutions replay true.
Hooray for solvable boards! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly describes the main change: guaranteeing solvable boards, adding order-strategy infrastructure, and including documentation. It accurately reflects the core deliverables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch solvable-board-generation

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/zustand/game-store.ts`:
- Around line 334-336: Remove the assignment to shouldAnimateOnMount from
redealCurrentLevel(): currently redealCurrentLevel() sets both boardGeneration
and shouldAnimateOnMount; change it to only update boardGeneration (and any
other necessary state) and do not touch shouldAnimateOnMount there so the flag
remains controlled exclusively by start() and levelCleared() flows; locate
redealCurrentLevel in the zustand store and remove the shouldAnimateOnMount
update while leaving boardGeneration: Date.now() unchanged.
- Around line 331-337: The redealCurrentLevel function replaces gameBoard but
doesn’t prevent previously scheduled animation timeouts (from functions like
clicked) from mutating the new board; modify redealCurrentLevel to either
increment/update the shared boardGeneration and ensure all animation callbacks
check that captured boardGeneration matches the current one before mutating
state, or track and clear outstanding timeout IDs before re-dealing; update the
animation callback in clicked (or wherever setTimeout is used) to capture the
boardGeneration value and no-op if it mismatches to prevent stale timeouts from
altering the new board.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e448c107-c253-4ea4-a8d7-676ca0ac0c54

📥 Commits

Reviewing files that changed from the base of the PR and between e5a3124 and df31934.

⛔ Files ignored due to path filters (3)
  • docs/gifs/construction.gif is excluded by !**/*.gif
  • docs/gifs/freeness.gif is excluded by !**/*.gif
  • docs/gifs/random.gif is excluded by !**/*.gif
📒 Files selected for processing (12)
  • CLAUDE.md
  • docs/solvable-boards.html
  • docs/strategy-difficulty-report.html
  • src/app/play/page.tsx
  • src/components/strategy-switcher/index.tsx
  • src/utils/board-rules.ts
  • src/utils/init-gameboard.ts
  • src/utils/order-strategies.test.ts
  • src/utils/order-strategies.ts
  • src/utils/shuffle.ts
  • src/utils/solvable-board.test.ts
  • src/zustand/game-store.ts

Comment thread src/zustand/game-store.ts
Comment on lines +331 to +337
redealCurrentLevel: () => {
set((state) => ({
gameBoard: initializeGameBoard(state.level),
boardGeneration: Date.now(),
shouldAnimateOnMount: true,
}));
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent stale animation timeouts from mutating a re-dealt board.

Line 331 enables board replacement while older clicked() animation timers may still fire. Those callbacks can delete indices from the new board and corrupt token parity/solvability. Guard timeout callbacks with a captured boardGeneration (or clear tracked timeout IDs before re-deal/restart).

💡 Proposed guard pattern
 clicked: async (index: string) => {
+  const generationAtClick = get().boardGeneration;
   set((state) => {
     const gameBoard = { ...state.gameBoard };
     const tile = gameBoard[index];
@@
             setTimeout(() => {
               set((state) => {
+                if (state.boardGeneration !== generationAtClick) return state;
                 const board = { ...state.gameBoard };
                 delete board[index];
                 delete board[otherActiveId];
@@
           setTimeout(() => {
             set((state) => {
+              if (state.boardGeneration !== generationAtClick) return state;
               const board = { ...state.gameBoard };
               if (board[otherActiveId]) {
                 board[otherActiveId] = { ...board[otherActiveId], animating: null };
@@
               setTimeout(() => {
                 set((state) => {
+                  if (state.boardGeneration !== generationAtClick) return state;
                   const board = { ...state.gameBoard };
                   if (board[index]?.animating === "sparkle") {
                     board[index] = { ...board[index], animating: null };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zustand/game-store.ts` around lines 331 - 337, The redealCurrentLevel
function replaces gameBoard but doesn’t prevent previously scheduled animation
timeouts (from functions like clicked) from mutating the new board; modify
redealCurrentLevel to either increment/update the shared boardGeneration and
ensure all animation callbacks check that captured boardGeneration matches the
current one before mutating state, or track and clear outstanding timeout IDs
before re-dealing; update the animation callback in clicked (or wherever
setTimeout is used) to capture the boardGeneration value and no-op if it
mismatches to prevent stale timeouts from altering the new board.

Comment thread src/zustand/game-store.ts
Comment on lines +334 to +336
boardGeneration: Date.now(),
shouldAnimateOnMount: true,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove shouldAnimateOnMount assignment from redealCurrentLevel().

Line 335 sets shouldAnimateOnMount during re-deal. This flag should stay constrained to start()/levelCleared() flows.

✂️ Minimal fix
       redealCurrentLevel: () => {
         set((state) => ({
           gameBoard: initializeGameBoard(state.level),
           boardGeneration: Date.now(),
-          shouldAnimateOnMount: true,
         }));
       },

As per coding guidelines: Set shouldAnimateOnMount: true only in start() and levelCleared() actions; do NOT set it during pause/resume operations.

📝 Committable suggestion

‼️ 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.

Suggested change
boardGeneration: Date.now(),
shouldAnimateOnMount: true,
}));
redealCurrentLevel: () => {
set((state) => ({
gameBoard: initializeGameBoard(state.level),
boardGeneration: Date.now(),
}));
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zustand/game-store.ts` around lines 334 - 336, Remove the assignment to
shouldAnimateOnMount from redealCurrentLevel(): currently redealCurrentLevel()
sets both boardGeneration and shouldAnimateOnMount; change it to only update
boardGeneration (and any other necessary state) and do not touch
shouldAnimateOnMount there so the flag remains controlled exclusively by start()
and levelCleared() flows; locate redealCurrentLevel in the zustand store and
remove the shouldAnimateOnMount update while leaving boardGeneration: Date.now()
unchanged.

Strategy switching is a dev-only affordance. In production getOrderStrategy
now ignores player-accessible overrides (runtime + localStorage) and the
window.eurekaOrder console hook is not exposed; production resolves
env (NEXT_PUBLIC_ORDER_STRATEGY) -> default only. This keeps highscores
comparable and prevents players from changing the board distribution they
are scored on. The dev switcher badge was already NODE_ENV-gated.
Persist which order strategy produced a board so highscores can be compared
within a strategy. The store captures getOrderStrategy().name whenever a board
is dealt (start / next level / re-deal) and threads it through
saveGameState -> /api/game -> the games row; the highscores query returns it
and the leaderboard shows it under each name.

Migration adds games.strategy (text, default 'random'): rows created before
solvability-by-construction used a plain random fill, so they backfill to
'random'; the default also keeps inserts from the previously-deployed client
valid. Applied to the project and verified (835 historical rows -> 'random').
@riethmayer

Copy link
Copy Markdown
Owner Author

Update: strategy locked in prod + recorded on highscores

Two follow-up commits:

  • feat: lock order strategy in production — strategy switching is dev-only. In production getOrderStrategy ignores player-accessible overrides (runtime + localStorage) and the window.eurekaOrder hook is not exposed; prod resolves NEXT_PUBLIC_ORDER_STRATEGY → default only. Keeps highscores comparable and un-gameable. (Switcher badge was already NODE_ENV-gated.)
  • feat: record the order strategy with each game — the store captures the active strategy when a board is dealt and threads it through saveGameState → /api/game → games.strategy; highscores query returns it and the leaderboard shows it under each name.

DB migration — already applied to the project (additive + idempotent): supabase/migrations/20260525_add_strategy_to_games.sql adds games.strategy text not null default 'random'. Verified: all 835 pre-existing rows backfilled to 'random'; a live save with strategy='scatter' round-trips correctly. Because it's additive with a default, the currently-deployed client (which omits the column) keeps inserting fine until this ships.

yarn test 101 pass · type-check + lint clean.

…ration

# Conflicts:
#	src/app/highscores/page.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/zustand/game-store.ts (2)

338-344: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard delayed tile callbacks against board-generation changes.

Re-deal swaps in a new board, but pending setTimeout callbacks in clicked() can still delete/reset tiles on that new board. This can corrupt the re-dealt state.

Suggested guard pattern
 clicked: async (index: string) => {
+  const generationAtClick = get().boardGeneration;
   set((state) => {
@@
             setTimeout(() => {
               set((state) => {
+                if (state.boardGeneration !== generationAtClick) return state;
                 const board = { ...state.gameBoard };
                 delete board[index];
                 delete board[otherActiveId];
@@
           setTimeout(() => {
             set((state) => {
+              if (state.boardGeneration !== generationAtClick) return state;
               const board = { ...state.gameBoard };
               if (board[otherActiveId]) {
                 board[otherActiveId] = { ...board[otherActiveId], animating: null };
@@
               setTimeout(() => {
                 set((state) => {
+                  if (state.boardGeneration !== generationAtClick) return state;
                   const board = { ...state.gameBoard };
                   if (board[index]?.animating === "sparkle") {
                     board[index] = { ...board[index], animating: null };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zustand/game-store.ts` around lines 338 - 344, When redealCurrentLevel()
replaces the board it sets boardGeneration; update clicked() so any delayed
setTimeout callbacks capture the current boardGeneration (e.g., const token =
get().boardGeneration or pass it into the closure) and early-return if
get().boardGeneration !== token before performing any tile deletion/reset
operations. Guard every delayed callback in clicked() that mutates gameBoard or
tiles using this boardGeneration token to prevent callbacks from affecting a
newly generated board.

338-344: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove shouldAnimateOnMount from redealCurrentLevel().

Keeping this flag true in re-deal broadens intro-animation triggers beyond the intended lifecycle.

Minimal fix
       redealCurrentLevel: () => {
         set((state) => ({
           gameBoard: initializeGameBoard(state.level),
           strategy: getOrderStrategy().name,
           boardGeneration: Date.now(),
-          shouldAnimateOnMount: true,
         }));
       },
As per coding guidelines: `Set shouldAnimateOnMount: true only in start() and levelCleared() actions; do NOT set it during pause/resume operations`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/zustand/game-store.ts` around lines 338 - 344, redealCurrentLevel
currently sets shouldAnimateOnMount to true which improperly retriggers intro
animations; remove the shouldAnimateOnMount assignment from the
redealCurrentLevel action in the game-store (function redealCurrentLevel) so it
no longer mutates that flag. Verify that only start() and levelCleared() actions
set shouldAnimateOnMount: true (adjust those methods if needed) and ensure
pause/resume/redeal paths do not touch shouldAnimateOnMount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/zustand/game-store.ts`:
- Around line 338-344: When redealCurrentLevel() replaces the board it sets
boardGeneration; update clicked() so any delayed setTimeout callbacks capture
the current boardGeneration (e.g., const token = get().boardGeneration or pass
it into the closure) and early-return if get().boardGeneration !== token before
performing any tile deletion/reset operations. Guard every delayed callback in
clicked() that mutates gameBoard or tiles using this boardGeneration token to
prevent callbacks from affecting a newly generated board.
- Around line 338-344: redealCurrentLevel currently sets shouldAnimateOnMount to
true which improperly retriggers intro animations; remove the
shouldAnimateOnMount assignment from the redealCurrentLevel action in the
game-store (function redealCurrentLevel) so it no longer mutates that flag.
Verify that only start() and levelCleared() actions set shouldAnimateOnMount:
true (adjust those methods if needed) and ensure pause/resume/redeal paths do
not touch shouldAnimateOnMount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5605015-8584-4af0-8523-b4bc1665a455

📥 Commits

Reviewing files that changed from the base of the PR and between df31934 and 59f40b5.

📒 Files selected for processing (7)
  • src/app/highscores/page.tsx
  • src/db/select-game.ts
  • src/types/game.ts
  • src/utils/order-strategies.ts
  • src/utils/post-game-state.ts
  • src/zustand/game-store.ts
  • supabase/migrations/20260525_add_strategy_to_games.sql
✅ Files skipped from review due to trivial changes (1)
  • supabase/migrations/20260525_add_strategy_to_games.sql

Master refactored /highscores to render via the Leaderboard client component,
so the per-row strategy label moves there. `strategy` is optional on
HighscoreRow so the optimistic "you" row, historical rows ('random'), and
master's existing test fixtures all type-check.

order-strategies test: fewer iterations + an explicit timeout so the
solvable-deal sweep doesn't exceed the default 5s on slower CI — which also
stops it from starving the real-timer autosave test in a parallel worker.
"autosaves every 60 seconds" could flake on slow CI: start() spins a live 1s
interval, so if the 59 manual step() calls took >1s the interval fired an
extra step and tripped the autosave at 60 early. Scope fake timers to the test
so only manual steps advance time. (Pre-existing flake; surfaced by CI running
the workflow twice and splitting pass/fail on identical code.)
The autosave flake's real cause was a cross-test leak, not the test's own
interval: start() spins a real 1s interval on the singleton store, and
beforeEach reset state without clearing live intervals, so intervals from
earlier tests kept firing step() on the shared store throughout the file —
intermittently pushing timePassed to 60 and tripping the autosave early.

Add a file-scope afterEach that clears any live timer after every test. With
that plus the autosave test's own fake timers, no real interval fires during
that test, so it's deterministic regardless of runner speed.
@riethmayer riethmayer merged commit 426cb0c into master May 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant