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:
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user