feat: Show solution — winning_line core API + auto-playback (card_game bucket 4) #159

Merged
funman300 merged 2 commits from feat/solution-line into master 2026-07-08 01:08:58 +00:00
Owner

Puts the previously unused Solution::clean_solution() to work as a player-facing feature.

CoreGameState::winning_line(moves_budget, states_budget): budgeted throwaway-session solve (mirrors the hint path), cleaned via clean_solution(), is_useless moves stripped, and the stripped line is verified by pure replay (falls back to the unstripped line if stripping ever broke it). Contract: a returned line always replays to a win via apply_instruction. 4 new tests; 57 core tests green.

EngineSolutionPlaybackPlugin: visible "Show solution" button in the pause modal (§3.3), solves off-thread on AsyncComputeTaskPool with stale-result protection, then steps the line at 0.45s/move through the normal MoveRequestEvent/DrawRequestEvent pipeline — GameStateResource stays the only mutator. Cancels on Esc/pause/undo/new-game/MoveRejectedEvent; toasts for searching/found/unwinnable/budget-exhausted. 4 headless tests incl. a full request→solve→auto-play→win end-to-end; engine 917/917, workspace green, clippy -D warnings, fmt.

Follow-up candidates (not in this PR): progress indicator during long solves; decide whether Esc should be swallowed during playback instead of also opening pause. Note: the pre-existing winnable_verdict_carries_a_first_move test is vacuous (passes on solver error) — filing separately.

🤖 Generated with Claude Code

Puts the previously unused `Solution::clean_solution()` to work as a player-facing feature. **Core** — `GameState::winning_line(moves_budget, states_budget)`: budgeted throwaway-session solve (mirrors the hint path), cleaned via `clean_solution()`, `is_useless` moves stripped, and the stripped line is verified by pure replay (falls back to the unstripped line if stripping ever broke it). Contract: a returned line always replays to a win via `apply_instruction`. 4 new tests; 57 core tests green. **Engine** — `SolutionPlaybackPlugin`: visible "Show solution" button in the pause modal (§3.3), solves off-thread on `AsyncComputeTaskPool` with stale-result protection, then steps the line at 0.45s/move through the normal `MoveRequestEvent`/`DrawRequestEvent` pipeline — `GameStateResource` stays the only mutator. Cancels on Esc/pause/undo/new-game/`MoveRejectedEvent`; toasts for searching/found/unwinnable/budget-exhausted. 4 headless tests incl. a full request→solve→auto-play→win end-to-end; engine 917/917, workspace green, clippy `-D warnings`, fmt. Follow-up candidates (not in this PR): progress indicator during long solves; decide whether Esc should be swallowed during playback instead of also opening pause. Note: the pre-existing `winnable_verdict_carries_a_first_move` test is vacuous (passes on solver error) — filing separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
funman300 added 2 commits 2026-07-07 23:34:32 +00:00
New GameState::winning_line(moves_budget, states_budget) returns the
complete instruction sequence to a win (Ok(None) when unwinnable or
already won, Err on budget exhaustion), compacting the raw DFS trace
with the previously unused card_game Solution::clean_solution and
stripping foundation→foundation no-ops when the stripped line still
replays to a win — an internal replay check guarantees the returned
sequence always applies cleanly via apply_instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SolutionPlaybackPlugin: the pause modal's 'Show solution' button
resumes the game and requests a solve of the live position via
GameState::winning_line on AsyncComputeTaskPool (never blocks the main
thread; stale results discarded via a move_count snapshot). The line
then plays back one instruction per 0.45 s through the normal
MoveRequestEvent / DrawRequestEvent pipeline — animations, scoring,
undo history and win detection behave as if the player made the moves.
Tableau run counts are decoded against the live state at step time.

Playback cancels on Esc, pause, undo / new-game requests, and any
rejected move (the signature of the player diverging the board).
Toasts cover search start, line found, unwinnable, and budget-
exhausted outcomes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
funman300 merged commit 251d35bc28 into master 2026-07-08 01:08:58 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: funman300/Ferrous-Solitaire#159