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
+16 -28
View File
@@ -60,27 +60,23 @@ fn draw_mode_strategy() -> impl Strategy<Value = DrawMode> {
///
/// Each action is `(draw_flag, move_index)`:
/// - `draw_flag = true` → call `game.draw()`
/// - `draw_flag = false` → pick the `move_index % len`th legal move from
/// `possible_instructions()` and execute it.
/// - `draw_flag = false` → pick the `move_index % len`th legal instruction
/// from `possible_instructions()` and apply it via `apply_instruction()`.
///
/// `possible_instructions()` may return `(Stock, Stock, 1)` for the
/// RotateStock / draw action. `move_cards(Stock, Stock, 1)` is rejected by
/// the `from == to` guard, so those are dispatched to `game.draw()`.
/// `possible_instructions()` may return `RotateStock`, which
/// `apply_instruction()` dispatches to `game.draw()`; ordinary instructions
/// are equivalent to `move_cards(from, to, count)`.
fn apply_random_actions(game: &mut GameState, actions: &[(bool, usize)]) {
for &(do_draw, idx) in actions {
if do_draw {
let _ = game.draw();
} else {
let instructions = game.possible_instructions();
if instructions.is_empty() {
let moves = game.possible_instructions();
if moves.is_empty() {
continue;
}
let (from, to, count) = instructions[idx % instructions.len()];
if from == to {
let _ = game.draw();
} else {
let _ = game.move_cards(from, to, count);
}
let instruction = moves[idx % moves.len()];
let _ = game.apply_instruction(instruction);
}
}
}
@@ -92,16 +88,12 @@ fn apply_one_move(game: &mut GameState, move_idx: usize) -> bool {
if game.is_won() {
return false;
}
let instructions = game.possible_instructions();
if instructions.is_empty() {
let moves = game.possible_instructions();
if moves.is_empty() {
return game.draw().is_ok();
}
let (from, to, count) = instructions[move_idx % instructions.len()];
if from == to {
game.draw().is_ok()
} else {
game.move_cards(from, to, count).is_ok()
}
let instruction = moves[move_idx % moves.len()];
game.apply_instruction(instruction).is_ok()
}
// ---------------------------------------------------------------------------
@@ -258,17 +250,13 @@ proptest! {
let mut game = GameState::new(seed, draw_mode);
apply_random_actions(&mut game, &setup_actions);
for (from, to, count) in game.possible_instructions() {
for instruction in game.possible_instructions() {
// Clone so each move is tried from the same starting state.
let mut trial = game.clone();
let result = if from == to {
trial.draw()
} else {
trial.move_cards(from, to, count)
};
let result = trial.apply_instruction(instruction);
prop_assert!(
result.is_ok(),
"possible_instructions() reported ({from:?} → {to:?} ×{count}) \
"possible_instructions() reported {instruction:?} \
as legal but the call returned Err: {result:?}",
);
}