refactor(core): make KlondikeInstruction the move currency
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:
@@ -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(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user