refactor: migrate PileType → KlondikePile across core/wasm/engine
Build and Deploy / build-and-push (push) Failing after 1m24s

- Replace PileType with typed KlondikePile (Foundation/Tableau variants)
  throughout solitaire_core, solitaire_wasm, and solitaire_engine;
  ReplayMove now uses SavedKlondikePile for serialisation stability
- Split replay_overlay.rs into replay_overlay/ module (mod, format,
  input, update, tests) for maintainability
- Add klondike dep to solitaire_engine and solitaire_data Cargo.toml
- Add TestPileState infrastructure to game_state.rs for engine unit tests
- Rebuild solitaire_wasm pkg (js + wasm artefacts updated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-01 13:13:35 -07:00
parent ca612f51f1
commit 9260ca7994
36 changed files with 7429 additions and 7064 deletions
+41 -25
View File
@@ -43,7 +43,7 @@ use std::hash::{Hash, Hasher};
use bevy::prelude::*;
use bevy::window::RequestRedraw;
use solitaire_core::pile::PileType;
use klondike::{Foundation, KlondikePile};
use solitaire_data::AnimSpeed;
use crate::animation_plugin::CardAnim;
@@ -246,10 +246,8 @@ fn start_shake_anim(
}
let dest_pile = &ev.to;
// Collect the card ids that belong to the destination pile.
let Some(pile) = game.0.piles.get(dest_pile) else {
continue;
};
let dest_card_ids: Vec<u32> = pile.cards.iter().map(|c| c.id).collect();
let dest_cards = pile_cards(&game.0, dest_pile);
let dest_card_ids: Vec<u32> = dest_cards.iter().map(|c| c.id).collect();
if dest_card_ids.is_empty() {
continue;
@@ -319,19 +317,19 @@ fn start_settle_anim(
let mut bounce_ids: Vec<u32> = Vec::new();
for ev in moves.read() {
if let Some(pile) = game.0.piles.get(&ev.to) {
let pile = pile_cards(&game.0, &ev.to);
if !pile.is_empty() {
// The moved cards land on top — take the last `count` ids.
let n = ev.count.min(pile.cards.len());
let n = ev.count.min(pile.len());
if n > 0 {
let start = pile.cards.len() - n;
bounce_ids.extend(pile.cards[start..].iter().map(|c| c.id));
let start = pile.len() - n;
bounce_ids.extend(pile[start..].iter().map(|c| c.id));
}
}
}
if draws.read().next().is_some()
&& let Some(pile) = game.0.piles.get(&PileType::Waste)
&& let Some(top) = pile.cards.last()
&& let Some(top) = game.0.waste_cards().last()
{
bounce_ids.push(top.id);
}
@@ -399,7 +397,7 @@ fn start_deal_anim(
return;
}
let Some(layout) = layout else { return };
let Some(&stock_pos) = layout.0.pile_positions.get(&PileType::Stock) else {
let Some(&stock_pos) = layout.0.pile_positions.get(&KlondikePile::Stock) else {
return;
};
let stock_start = Vec3::new(stock_pos.x, stock_pos.y, 0.0);
@@ -520,15 +518,13 @@ fn start_foundation_flourish(
if reduce_motion {
continue;
}
let pile_type = PileType::Foundation(ev.slot);
let Some(foundation) = foundation_from_slot(ev.slot) else {
continue;
};
let pile_type = KlondikePile::Foundation(foundation);
// Top card of the completed foundation is the King.
let Some(king_id) = game
.0
.piles
.get(&pile_type)
.and_then(|p| p.cards.last())
.map(|c| c.id)
else {
let cards = game.0.pile(pile_type);
let Some(king_id) = cards.last().map(|c| c.id) else {
continue;
};
@@ -634,6 +630,26 @@ fn lerp_color(from: Color, to: Color, t: f32) -> Color {
)
}
fn pile_cards(
game: &solitaire_core::game_state::GameState,
pile: &KlondikePile,
) -> Vec<solitaire_core::card::Card> {
match pile {
KlondikePile::Stock => game.waste_cards(),
_ => game.pile(*pile),
}
}
fn foundation_from_slot(slot: u8) -> Option<Foundation> {
match slot {
0 => Some(Foundation::Foundation1),
1 => Some(Foundation::Foundation2),
2 => Some(Foundation::Foundation3),
3 => Some(Foundation::Foundation4),
_ => None,
}
}
// ---------------------------------------------------------------------------
// Unit tests (pure functions only — no Bevy world required)
// ---------------------------------------------------------------------------
@@ -834,6 +850,7 @@ mod tests {
fn shake_anim_skipped_under_reduce_motion() {
use bevy::ecs::message::Messages;
use solitaire_core::game_state::{DrawMode, GameState};
use klondike::Tableau;
use solitaire_data::Settings;
let mut app = App::new();
@@ -847,14 +864,13 @@ mod tests {
app.update();
// Pick a card from Tableau(0) so the event refers to a real pile.
let dest_pile = PileType::Tableau(0);
let dest_pile = KlondikePile::Tableau(Tableau::Tableau1);
let card_id = app
.world()
.resource::<GameStateResource>()
.0
.piles
.get(&dest_pile)
.and_then(|p| p.cards.last())
.pile(dest_pile)
.last()
.map(|c| c.id)
.expect("Tableau(0) should have at least one card in a fresh game");
@@ -866,7 +882,7 @@ mod tests {
app.world_mut()
.resource_mut::<Messages<MoveRejectedEvent>>()
.write(MoveRejectedEvent {
from: PileType::Stock,
from: KlondikePile::Stock,
to: dest_pile,
count: 1,
});