refactor(core): make KlondikeInstruction the move currency
Build and Deploy / build-and-push (push) Failing after 1m1s
Web E2E / web-e2e (push) Failing after 3m26s

Remove the (from, to, count) tuple as an internal move-passing wrapper.
Game logic now stays in KlondikeInstruction space end to end:

- Add GameState::apply_instruction, the native apply path. move_cards
  becomes a thin pile-coordinate adapter that converts to an instruction
  and delegates, so move bookkeeping (validation, score/recycle history,
  undo snapshot) lives in one place instead of being duplicated.
- next_auto_complete_move matches DstFoundation directly instead of
  projecting every candidate to pile coordinates.
- proptests and the storage round-trip test apply instructions directly
  rather than round-tripping instruction -> tuple -> move_cards.

The single instruction -> pile decode is renamed instruction_to_highlight
-> instruction_to_piles and kept in core: decoding a tableau run length
needs upstream pile-stack types core does not re-export, so relocating it
would duplicate the logic across engine and wasm. The two rendering edges
(engine hint highlight, wasm debug move list) call this one decoder; the
engine's hint_piles is a thin delegation to it.

Also includes the CardEntityIndex render-side index and a SelectionPlugin
init_resource fix so update_selection_highlight no longer panics in test
harnesses that omit CardPlugin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-10 16:58:28 -07:00
parent dc4cf45ea0
commit ef1efdc3b5
9 changed files with 219 additions and 110 deletions
+3 -6
View File
@@ -30,7 +30,7 @@ use solitaire_data::solver::try_solve_from_state;
use crate::card_plugin::CardEntity;
use crate::events::{HintVisualEvent, InfoToastEvent, StateChangedEvent};
use crate::input_plugin::{emit_hint_visuals, find_heuristic_hint};
use crate::input_plugin::{emit_hint_visuals, find_heuristic_hint, hint_piles};
use crate::resources::{GameStateResource, HintCycleIndex};
/// In-flight async work for the H-key hint.
@@ -93,7 +93,7 @@ struct HintTask {
enum HintTaskOutput {
/// Solver verdict was winnable; here is the first move on the solution
/// path. Converted to highlighted `(from, to)` piles by the poll system
/// via [`GameState::instruction_to_move`].
/// via [`crate::input_plugin::hint_piles`].
SolverMove(KlondikeInstruction),
/// Solver was `Unwinnable` or `Inconclusive`. The poll system
/// runs the legacy heuristic against the live `GameState` so the
@@ -153,10 +153,7 @@ pub fn poll_pending_hint_task(
// Resolve the solver's first move to highlighted piles; fall back to the
// live-state heuristic when there's no solver move or it maps to a no-op.
let solver_pair = match output {
HintTaskOutput::SolverMove(instruction) => g
.0
.instruction_to_move(instruction)
.map(|(from, to, _count)| (from, to)),
HintTaskOutput::SolverMove(instruction) => hint_piles(&g.0, instruction),
HintTaskOutput::NeedsHeuristic => None,
};
let (from, to) = match solver_pair.or_else(|| find_heuristic_hint(&g.0, &mut hint_cycle)) {