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
+80 -35
View File
@@ -6,8 +6,8 @@
use bevy::prelude::*;
use bevy::window::WindowResized;
use klondike::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::Suit;
use solitaire_core::pile::PileType;
use crate::events::{HintVisualEvent, StateChangedEvent};
use crate::hud_plugin::HudVisibility;
@@ -54,7 +54,7 @@ pub struct TableBackground;
/// Marker component attached to each of the 13 empty-pile placeholders.
#[derive(Component, Debug, Clone)]
pub struct PileMarker(pub PileType);
pub struct PileMarker(pub KlondikePile);
/// Attached to a `PileMarker` entity when it has been temporarily tinted gold
/// as a hint destination. Stores the remaining countdown and the original sprite
@@ -265,14 +265,13 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
let marker_size = layout.card_size;
let font_size = layout.card_size.x * 0.28;
let mut piles: Vec<PileType> = Vec::with_capacity(13);
piles.push(PileType::Stock);
piles.push(PileType::Waste);
for slot in 0..4_u8 {
piles.push(PileType::Foundation(slot));
let mut piles: Vec<KlondikePile> = Vec::with_capacity(12);
piles.push(KlondikePile::Stock);
for foundation in foundations() {
piles.push(KlondikePile::Foundation(foundation));
}
for i in 0..7 {
piles.push(PileType::Tableau(i));
for tableau in tableaus() {
piles.push(KlondikePile::Tableau(tableau));
}
for pile in piles {
@@ -284,14 +283,14 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
..default()
},
Transform::from_xyz(pos.x, pos.y, Z_PILE_MARKER),
PileMarker(pile.clone()),
PileMarker(pile),
));
// Tableau markers show "K" (only a King may start an empty column).
// Foundation markers show "A" (only an Ace may claim an empty slot).
// Neither label carries a suit because any suit may start any slot.
match &pile {
PileType::Tableau(_) => {
KlondikePile::Tableau(_) => {
entity.with_children(|b| {
b.spawn((
Text2d::new("K"),
@@ -304,7 +303,7 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
));
});
}
PileType::Foundation(_) => {
KlondikePile::Foundation(_) => {
entity.with_children(|b| {
b.spawn((
Text2d::new("A"),
@@ -480,11 +479,7 @@ fn sync_pile_marker_visibility(
return;
}
for (pile_marker, mut visibility) in markers.iter_mut() {
let is_empty = game
.0
.piles
.get(&pile_marker.0)
.is_none_or(|pile| pile.cards.is_empty());
let is_empty = pile_cards(&game.0, &pile_marker.0).is_empty();
*visibility = if is_empty {
Visibility::Inherited
} else {
@@ -493,6 +488,44 @@ fn sync_pile_marker_visibility(
}
}
fn pile_cards(
game: &solitaire_core::game_state::GameState,
pile: &KlondikePile,
) -> Vec<solitaire_core::card::Card> {
match pile {
KlondikePile::Stock => {
let stock = game.stock_cards();
if stock.is_empty() {
game.waste_cards()
} else {
stock
}
}
_ => game.pile(*pile),
}
}
const fn foundations() -> [Foundation; 4] {
[
Foundation::Foundation1,
Foundation::Foundation2,
Foundation::Foundation3,
Foundation::Foundation4,
]
}
const fn tableaus() -> [Tableau; 7] {
[
Tableau::Tableau1,
Tableau::Tableau2,
Tableau::Tableau3,
Tableau::Tableau4,
Tableau::Tableau5,
Tableau::Tableau6,
Tableau::Tableau7,
]
}
#[cfg(test)]
mod tests {
use super::*;
@@ -510,14 +543,14 @@ mod tests {
}
#[test]
fn table_plugin_spawns_thirteen_pile_markers() {
fn table_plugin_spawns_twelve_pile_markers() {
let mut app = headless_app();
let count = app
.world_mut()
.query::<&PileMarker>()
.iter(app.world())
.count();
assert_eq!(count, 13);
assert_eq!(count, 12);
}
#[test]
@@ -540,7 +573,7 @@ mod tests {
#[test]
fn every_pile_marker_has_unique_type() {
let mut app = headless_app();
let mut types: Vec<PileType> = app
let mut types: Vec<KlondikePile> = app
.world_mut()
.query::<&PileMarker>()
.iter(app.world())
@@ -548,15 +581,15 @@ mod tests {
.collect();
types.sort_by_key(|p| format!("{p:?}"));
types.dedup();
assert_eq!(types.len(), 13);
assert_eq!(types.len(), 12);
}
#[test]
fn pile_markers_hide_when_pile_is_occupied() {
// After a fresh deal: the 7 tableau piles + the stock pile are
// all occupied; the 4 foundation piles + the waste pile are
// empty. The visibility-by-occupancy system must hide the
// first 8 markers and keep the last 5 visible. This implements
// occupied; the 4 foundation piles are empty. The visibility-by-
// occupancy system must hide the first 8 markers and keep the
// last 4 visible. This implements
// the "remain visible only where a pile is empty" invariant
// in the module-level doc comment that was previously
// declared but not enforced — pile markers used to always
@@ -570,8 +603,8 @@ mod tests {
app.update();
let mut q = app.world_mut().query::<(&PileMarker, &Visibility)>();
let mut hidden_piles: Vec<PileType> = Vec::new();
let mut visible_piles: Vec<PileType> = Vec::new();
let mut hidden_piles: Vec<KlondikePile> = Vec::new();
let mut visible_piles: Vec<KlondikePile> = Vec::new();
for (marker, visibility) in q.iter(app.world()) {
if matches!(visibility, Visibility::Hidden) {
hidden_piles.push(marker.0.clone());
@@ -586,19 +619,31 @@ mod tests {
8,
"stock + 7 tableau piles should hide their markers post-deal",
);
assert!(hidden_piles.contains(&PileType::Stock));
for i in 0..7 {
assert!(hidden_piles.contains(&KlondikePile::Stock));
for tableau in [
Tableau::Tableau1,
Tableau::Tableau2,
Tableau::Tableau3,
Tableau::Tableau4,
Tableau::Tableau5,
Tableau::Tableau6,
Tableau::Tableau7,
] {
assert!(
hidden_piles.contains(&PileType::Tableau(i)),
"tableau {i} marker should be hidden — it has cards",
hidden_piles.contains(&KlondikePile::Tableau(tableau)),
"{tableau:?} marker should be hidden — it has cards",
);
}
// 5 empty piles: waste + 4 foundations.
assert_eq!(visible_piles.len(), 5);
assert!(visible_piles.contains(&PileType::Waste));
for i in 0..4_u8 {
assert!(visible_piles.contains(&PileType::Foundation(i)));
// 4 empty piles: foundations only.
assert_eq!(visible_piles.len(), 4);
for foundation in [
Foundation::Foundation1,
Foundation::Foundation2,
Foundation::Foundation3,
Foundation::Foundation4,
] {
assert!(visible_piles.contains(&KlondikePile::Foundation(foundation)));
}
}