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
+22 -21
View File
@@ -41,7 +41,7 @@ use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::Card;
use solitaire_core::game_state::GameState;
use crate::card_plugin::CardEntity;
use crate::card_plugin::CardEntityIndex;
use crate::events::{InfoToastEvent, MoveRequestEvent, StateChangedEvent};
use crate::game_plugin::GameMutation;
use crate::input_plugin::{best_destination, best_tableau_destination_for_stack};
@@ -147,8 +147,12 @@ pub struct SelectionPlugin;
impl Plugin for SelectionPlugin {
fn build(&self, app: &mut App) {
// `CardEntityIndex` is owned and kept current by `CardPlugin`; this
// call is a no-op there. It is declared here so `update_selection_highlight`
// can read it via `Res<>` even in harnesses that omit `CardPlugin`.
app.init_resource::<SelectionState>()
.init_resource::<KeyboardDragState>()
.init_resource::<CardEntityIndex>()
.add_systems(
Update,
(
@@ -657,7 +661,7 @@ fn update_selection_highlight(
kbd_drag: Res<KeyboardDragState>,
game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>,
card_entities: Query<(Entity, &CardEntity)>,
card_index: Res<CardEntityIndex>,
highlights: Query<Entity, With<SelectionHighlight>>,
) {
// Always despawn any existing highlight first.
@@ -695,7 +699,7 @@ fn update_selection_highlight(
{
spawn_highlight_on_card(
&mut commands,
&card_entities,
&card_index,
&card,
card_size,
source_color,
@@ -712,7 +716,7 @@ fn update_selection_highlight(
if let Some(card) = top_face_up_card(dest, &game.0) {
spawn_highlight_on_card(
&mut commands,
&card_entities,
&card_index,
&card,
card_size,
dest_color,
@@ -741,27 +745,24 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
/// the matching `CardEntity::card`. No-op if no entity matches.
fn spawn_highlight_on_card(
commands: &mut Commands,
card_entities: &Query<(Entity, &CardEntity)>,
card_index: &CardEntityIndex,
card: &Card,
card_size: Vec2,
color: Color,
) {
for (entity, card_entity) in card_entities {
if card_entity.card == *card {
commands.entity(entity).with_children(|b| {
b.spawn((
SelectionHighlight,
Sprite {
color,
custom_size: Some(card_size + Vec2::splat(4.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, -0.01),
Visibility::default(),
));
});
break;
}
if let Some(entity) = card_index.get(card) {
commands.entity(entity).with_children(|b| {
b.spawn((
SelectionHighlight,
Sprite {
color,
custom_size: Some(card_size + Vec2::splat(4.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, -0.01),
Visibility::default(),
));
});
}
}