refactor: migrate PileType → KlondikePile across core/wasm/engine
Build and Deploy / build-and-push (push) Failing after 1m24s
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:
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user