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
+58 -59
View File
@@ -34,8 +34,8 @@
use bevy::prelude::*;
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
use klondike::{Foundation, KlondikePile, Tableau};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::pile::PileType;
use crate::card_plugin::RightClickHighlight;
use crate::layout::{Layout, LayoutResource};
@@ -65,10 +65,10 @@ const MARKER_VALID: Color = Color::srgba(0.675, 0.761, 0.404, 0.55);
/// Marker component on a parent entity that owns one drop-target overlay
/// (a translucent fill plus four outline edges as children). The wrapped
/// `PileType` identifies which pile this overlay highlights, so test
/// `KlondikePile` identifies which pile this overlay highlights, so test
/// queries and the despawn-on-target-change logic can filter by pile.
#[derive(Component, Debug, Clone, PartialEq, Eq)]
pub struct DropTargetOverlay(pub PileType);
pub struct DropTargetOverlay(pub KlondikePile);
/// Renders a custom cursor sprite that follows the pointer and swaps to a grab-hand icon while a card drag is in progress.
pub struct CursorPlugin;
@@ -162,33 +162,34 @@ fn update_cursor_icon(
/// Returns `true` if `cursor` (world-space) is over any face-up draggable card.
fn cursor_over_draggable(cursor: Vec2, game: &GameState, layout: &Layout) -> bool {
let piles = [
PileType::Waste,
PileType::Foundation(0),
PileType::Foundation(1),
PileType::Foundation(2),
PileType::Foundation(3),
PileType::Tableau(0),
PileType::Tableau(1),
PileType::Tableau(2),
PileType::Tableau(3),
PileType::Tableau(4),
PileType::Tableau(5),
PileType::Tableau(6),
KlondikePile::Stock,
KlondikePile::Foundation(Foundation::Foundation1),
KlondikePile::Foundation(Foundation::Foundation2),
KlondikePile::Foundation(Foundation::Foundation3),
KlondikePile::Foundation(Foundation::Foundation4),
KlondikePile::Tableau(Tableau::Tableau1),
KlondikePile::Tableau(Tableau::Tableau2),
KlondikePile::Tableau(Tableau::Tableau3),
KlondikePile::Tableau(Tableau::Tableau4),
KlondikePile::Tableau(Tableau::Tableau5),
KlondikePile::Tableau(Tableau::Tableau6),
KlondikePile::Tableau(Tableau::Tableau7),
];
for pile in piles {
let Some(pile_cards) = game.piles.get(&pile) else {
let pile_cards = pile_cards(game, &pile);
if pile_cards.is_empty() {
continue;
};
let is_tableau = matches!(pile, PileType::Tableau(_));
}
let is_tableau = matches!(pile, KlondikePile::Tableau(_));
let base = layout.pile_positions[&pile];
for (i, card) in pile_cards.cards.iter().enumerate().rev() {
for (i, card) in pile_cards.iter().enumerate().rev() {
if !card.face_up {
continue;
}
// Only the topmost card is draggable on non-tableau piles.
if !is_tableau && i != pile_cards.cards.len() - 1 {
if !is_tableau && i != pile_cards.len() - 1 {
continue;
}
let pos = tableau_or_stack_pos(game, layout, &pile, i, base, is_tableau);
@@ -280,24 +281,24 @@ fn update_drop_target_overlays(
// Iterate the same pile list as `update_drop_highlights`. Stock and
// Waste are excluded because they are never legal drop targets.
let candidates = [
PileType::Foundation(0),
PileType::Foundation(1),
PileType::Foundation(2),
PileType::Foundation(3),
PileType::Tableau(0),
PileType::Tableau(1),
PileType::Tableau(2),
PileType::Tableau(3),
PileType::Tableau(4),
PileType::Tableau(5),
PileType::Tableau(6),
KlondikePile::Foundation(Foundation::Foundation1),
KlondikePile::Foundation(Foundation::Foundation2),
KlondikePile::Foundation(Foundation::Foundation3),
KlondikePile::Foundation(Foundation::Foundation4),
KlondikePile::Tableau(Tableau::Tableau1),
KlondikePile::Tableau(Tableau::Tableau2),
KlondikePile::Tableau(Tableau::Tableau3),
KlondikePile::Tableau(Tableau::Tableau4),
KlondikePile::Tableau(Tableau::Tableau5),
KlondikePile::Tableau(Tableau::Tableau6),
KlondikePile::Tableau(Tableau::Tableau7),
];
// Compute the new set of valid piles for this frame.
let mut valid: Vec<PileType> = Vec::new();
let mut valid: Vec<KlondikePile> = Vec::new();
for pile in &candidates {
if game.0.can_move_cards(origin, pile, drag_count) {
valid.push(pile.clone());
valid.push(*pile);
}
}
@@ -309,9 +310,9 @@ fn update_drop_target_overlays(
}
// Spawn overlays for piles that are now valid but don't yet have one.
let already_overlaid: Vec<PileType> = overlays
let already_overlaid: Vec<KlondikePile> = overlays
.iter()
.map(|(_, m)| m.0.clone())
.map(|(_, m)| m.0)
.filter(|p| valid.contains(p))
.collect();
@@ -330,10 +331,10 @@ fn update_drop_target_overlays(
/// for everything else it is card-sized. Replicated here rather than
/// imported because `pile_drop_rect` is private to `input_plugin` and
/// this overlay is the only other consumer.
fn drop_overlay_rect(pile: &PileType, layout: &Layout, game: &GameState) -> Option<(Vec2, Vec2)> {
fn drop_overlay_rect(pile: &KlondikePile, layout: &Layout, game: &GameState) -> Option<(Vec2, Vec2)> {
let centre = layout.pile_positions.get(pile).copied()?;
if matches!(pile, PileType::Tableau(_)) {
let card_count = game.piles.get(pile).map_or(0, |p| p.cards.len());
if matches!(pile, KlondikePile::Tableau(_)) {
let card_count = game.pile(*pile).len();
if card_count > 1 {
let fan = -layout.card_size.y * layout.tableau_fan_frac;
let bottom_card_centre_y = centre.y + fan * (card_count - 1) as f32;
@@ -354,7 +355,7 @@ fn drop_overlay_rect(pile: &PileType, layout: &Layout, game: &GameState) -> Opti
/// the appropriate world position for `pile`.
fn spawn_drop_target_overlay(
commands: &mut Commands,
pile: &PileType,
pile: &KlondikePile,
layout: &Layout,
game: &GameState,
) {
@@ -372,7 +373,7 @@ fn spawn_drop_target_overlay(
..default()
},
Transform::from_xyz(centre.x, centre.y, Z_DROP_OVERLAY),
DropTargetOverlay(pile.clone()),
DropTargetOverlay(*pile),
))
.with_children(|parent| {
// Top edge.
@@ -421,7 +422,7 @@ fn spawn_drop_target_overlay(
fn tableau_or_stack_pos(
game: &GameState,
layout: &Layout,
pile: &PileType,
pile: &KlondikePile,
index: usize,
base: Vec2,
is_tableau: bool,
@@ -431,8 +432,8 @@ fn tableau_or_stack_pos(
base.x,
base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32),
)
} else if matches!(pile, PileType::Waste) && game.draw_mode == DrawMode::DrawThree {
let pile_len = game.piles.get(pile).map_or(0, |p| p.cards.len());
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode == DrawMode::DrawThree {
let pile_len = game.waste_cards().len();
let visible_start = pile_len.saturating_sub(3);
let slot = index.saturating_sub(visible_start) as f32;
Vec2::new(base.x + slot * layout.card_size.x * 0.28, base.y)
@@ -441,6 +442,14 @@ fn tableau_or_stack_pos(
}
}
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<solitaire_core::card::Card> {
if matches!(pile, KlondikePile::Stock) {
game.waste_cards()
} else {
game.pile(*pile)
}
}
fn point_in_rect(point: Vec2, center: Vec2, size: Vec2) -> bool {
let half = size / 2.0;
point.x >= center.x - half.x
@@ -591,12 +600,8 @@ mod tests {
/// card. Used to make a specific tableau column accept a chosen
/// drag stack.
fn set_tableau_top(game: &mut GameState, idx: usize, card: Card) {
let pile = game
.piles
.get_mut(&PileType::Tableau(idx))
.expect("tableau pile exists");
pile.cards.clear();
pile.cards.push(card);
let tableau = GameState::tableau_from_index(idx).expect("tableau pile exists");
game.set_test_tableau_cards(tableau, vec![card]);
}
/// Inserts a single face-up dragged card into the waste pile and
@@ -606,17 +611,11 @@ mod tests {
// Place the dragged card on the waste pile (origin).
{
let mut game = app.world_mut().resource_mut::<GameStateResource>();
let waste = game
.0
.piles
.get_mut(&PileType::Waste)
.expect("waste pile exists");
waste.cards.clear();
waste.cards.push(dragged.clone());
game.0.set_test_waste_cards(vec![dragged.clone()]);
}
let mut drag = app.world_mut().resource_mut::<DragState>();
drag.cards = vec![dragged.id];
drag.origin_pile = Some(PileType::Waste);
drag.origin_pile = Some(KlondikePile::Stock);
drag.committed = true;
}
@@ -648,14 +647,14 @@ mod tests {
app.update();
let overlays: Vec<PileType> = app
let overlays: Vec<KlondikePile> = app
.world_mut()
.query::<&DropTargetOverlay>()
.iter(app.world())
.map(|o| o.0.clone())
.collect();
assert!(
!overlays.contains(&PileType::Tableau(2)),
!overlays.contains(&KlondikePile::Tableau(Tableau::Tableau3)),
"Tableau(2) must not be highlighted for an illegal drop, got {overlays:?}"
);
}