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:
@@ -47,9 +47,9 @@ use bevy::input::touch::Touches;
|
||||
use bevy::math::Vec2;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
use klondike::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::card::Card;
|
||||
use solitaire_core::game_state::GameState;
|
||||
use solitaire_core::pile::PileType;
|
||||
|
||||
use crate::card_plugin::TABLEAU_FACEDOWN_FAN_FRAC;
|
||||
use crate::events::{MoveRejectedEvent, MoveRequestEvent};
|
||||
@@ -107,7 +107,7 @@ pub enum RightClickRadialState {
|
||||
/// `hovered_index` (or none).
|
||||
Active {
|
||||
/// Pile the right-clicked card came from.
|
||||
source_pile: PileType,
|
||||
source_pile: KlondikePile,
|
||||
/// Number of cards that would be moved (always `1` — only the
|
||||
/// top face-up card is ever offered for a quick-drop, since the
|
||||
/// radial is built around single-card foundation/tableau
|
||||
@@ -122,7 +122,7 @@ pub enum RightClickRadialState {
|
||||
/// [`RADIAL_RADIUS_PX`] centred on the press position. A single
|
||||
/// destination is placed directly above the cursor; multiple
|
||||
/// destinations span an arc.
|
||||
legal_destinations: Vec<(PileType, Vec2)>,
|
||||
legal_destinations: Vec<(KlondikePile, Vec2)>,
|
||||
/// Cursor position (world space) the radial was opened at —
|
||||
/// used as the centre of the ring for cursor-hover hit testing.
|
||||
centre: Vec2,
|
||||
@@ -250,18 +250,18 @@ pub fn radial_hovered_index(cursor: Vec2, anchors: &[Vec2]) -> Option<usize> {
|
||||
/// dropping a card on its own pile is a no-op.
|
||||
pub fn legal_destinations_for_card(
|
||||
_card: &Card,
|
||||
source_pile: &PileType,
|
||||
source_pile: &KlondikePile,
|
||||
game: &GameState,
|
||||
) -> Vec<PileType> {
|
||||
) -> Vec<KlondikePile> {
|
||||
let mut out = Vec::new();
|
||||
for slot in 0..4_u8 {
|
||||
let dest = PileType::Foundation(slot);
|
||||
for foundation in foundations() {
|
||||
let dest = KlondikePile::Foundation(foundation);
|
||||
if game.can_move_cards(source_pile, &dest, 1) {
|
||||
out.push(dest);
|
||||
}
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
let dest = PileType::Tableau(i);
|
||||
for tableau in tableaus() {
|
||||
let dest = KlondikePile::Tableau(tableau);
|
||||
if game.can_move_cards(source_pile, &dest, 1) {
|
||||
out.push(dest);
|
||||
}
|
||||
@@ -281,36 +281,34 @@ pub fn find_top_face_up_card_at(
|
||||
cursor: Vec2,
|
||||
game: &GameState,
|
||||
layout: &Layout,
|
||||
) -> Option<(PileType, Card)> {
|
||||
) -> Option<(KlondikePile, Card)> {
|
||||
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 {
|
||||
continue;
|
||||
};
|
||||
if pile_cards.cards.is_empty() {
|
||||
let pile_cards = pile_cards(game, &pile);
|
||||
if pile_cards.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let is_tableau = matches!(pile, PileType::Tableau(_));
|
||||
for i in (0..pile_cards.cards.len()).rev() {
|
||||
let card = &pile_cards.cards[i];
|
||||
let is_tableau = matches!(pile, KlondikePile::Tableau(_));
|
||||
for i in (0..pile_cards.len()).rev() {
|
||||
let card = &pile_cards[i];
|
||||
if !card.face_up {
|
||||
continue;
|
||||
}
|
||||
// Only the top 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 = card_position(game, layout, &pile, i);
|
||||
@@ -331,19 +329,17 @@ pub fn find_top_face_up_card_at(
|
||||
/// Mirror of `input_plugin::card_position` — kept private to this
|
||||
/// module so the radial's hit-test geometry tracks renderer geometry
|
||||
/// without depending on `input_plugin` internals.
|
||||
fn card_position(game: &GameState, layout: &Layout, pile: &PileType, stack_index: usize) -> Vec2 {
|
||||
fn card_position(game: &GameState, layout: &Layout, pile: &KlondikePile, stack_index: usize) -> Vec2 {
|
||||
let base = layout.pile_positions[pile];
|
||||
if matches!(pile, PileType::Tableau(_)) {
|
||||
if matches!(pile, KlondikePile::Tableau(_)) {
|
||||
let mut y_offset = 0.0_f32;
|
||||
if let Some(pile_cards) = game.piles.get(pile) {
|
||||
for card in pile_cards.cards.iter().take(stack_index) {
|
||||
let step = if card.face_up {
|
||||
TABLEAU_FAN_FRAC
|
||||
} else {
|
||||
TABLEAU_FACEDOWN_FAN_FRAC
|
||||
};
|
||||
y_offset -= layout.card_size.y * step;
|
||||
}
|
||||
for card in pile_cards(game, pile).iter().take(stack_index) {
|
||||
let step = if card.face_up {
|
||||
TABLEAU_FAN_FRAC
|
||||
} else {
|
||||
TABLEAU_FACEDOWN_FAN_FRAC
|
||||
};
|
||||
y_offset -= layout.card_size.y * step;
|
||||
}
|
||||
Vec2::new(base.x, base.y + y_offset)
|
||||
} else {
|
||||
@@ -351,8 +347,36 @@ fn card_position(game: &GameState, layout: &Layout, pile: &PileType, stack_index
|
||||
}
|
||||
}
|
||||
|
||||
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<Card> {
|
||||
match pile {
|
||||
KlondikePile::Stock => game.waste_cards(),
|
||||
_ => 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,
|
||||
]
|
||||
}
|
||||
|
||||
/// Builds the `(destination, anchor)` list for a fresh radial open.
|
||||
fn build_radial_destinations(centre: Vec2, dests: Vec<PileType>) -> Vec<(PileType, Vec2)> {
|
||||
fn build_radial_destinations(centre: Vec2, dests: Vec<KlondikePile>) -> Vec<(KlondikePile, Vec2)> {
|
||||
let count = dests.len();
|
||||
dests
|
||||
.into_iter()
|
||||
@@ -442,7 +466,7 @@ fn radial_open_on_right_click(
|
||||
if dests.is_empty() {
|
||||
// No legal destinations — shake the source pile as feedback.
|
||||
rejected.write(MoveRejectedEvent {
|
||||
from: source_pile.clone(),
|
||||
from: source_pile,
|
||||
to: source_pile,
|
||||
count: 1,
|
||||
});
|
||||
@@ -606,8 +630,8 @@ fn radial_handle_release_or_cancel(
|
||||
&& let Some((dest, _)) = legal_destinations.get(*idx)
|
||||
{
|
||||
moves.write(MoveRequestEvent {
|
||||
from: source_pile.clone(),
|
||||
to: dest.clone(),
|
||||
from: *source_pile,
|
||||
to: *dest,
|
||||
count: *count,
|
||||
});
|
||||
}
|
||||
@@ -769,33 +793,37 @@ mod tests {
|
||||
fn ace_only_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
// Wipe everything.
|
||||
g.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
g.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
for slot in 0..4_u8 {
|
||||
g.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
g.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
g.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
// Ace of Clubs on Tableau(0).
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(0))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(CoreCard {
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
});
|
||||
}],
|
||||
);
|
||||
g
|
||||
}
|
||||
|
||||
@@ -803,32 +831,36 @@ mod tests {
|
||||
/// must skip it.
|
||||
fn face_down_only_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
g.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
g.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
for slot in 0..4_u8 {
|
||||
g.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
g.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
g.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(0))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(CoreCard {
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::King,
|
||||
face_up: false,
|
||||
});
|
||||
}],
|
||||
);
|
||||
g
|
||||
}
|
||||
|
||||
@@ -926,14 +958,14 @@ mod tests {
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
};
|
||||
let dests = legal_destinations_for_card(&card, &PileType::Tableau(0), &g);
|
||||
let dests = legal_destinations_for_card(&card, &KlondikePile::Tableau(Tableau::Tableau1), &g);
|
||||
// Ace can be placed on every empty foundation. We only need
|
||||
// the count to be ≥ 1 and the source pile to be excluded.
|
||||
assert!(
|
||||
!dests.is_empty(),
|
||||
"Ace must have at least one legal destination"
|
||||
);
|
||||
assert!(!dests.contains(&PileType::Tableau(0)));
|
||||
assert!(!dests.contains(&KlondikePile::Tableau(Tableau::Tableau1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -945,8 +977,8 @@ mod tests {
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
};
|
||||
let dests = legal_destinations_for_card(&card, &PileType::Foundation(0), &g);
|
||||
assert!(!dests.contains(&PileType::Foundation(0)));
|
||||
let dests = legal_destinations_for_card(&card, &KlondikePile::Foundation(Foundation::Foundation1), &g);
|
||||
assert!(!dests.contains(&KlondikePile::Foundation(Foundation::Foundation1)));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -963,7 +995,7 @@ mod tests {
|
||||
let mut app = radial_test_app();
|
||||
let layout_window = Vec2::new(1280.0, 800.0);
|
||||
let layout = compute_layout(layout_window, 0.0, 0.0, true);
|
||||
let ace_pos = layout.pile_positions[&PileType::Tableau(0)];
|
||||
let ace_pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau1)];
|
||||
|
||||
install_resources(&mut app, ace_only_state(), layout_window, ace_pos);
|
||||
press(&mut app, MouseButton::Right);
|
||||
@@ -990,7 +1022,7 @@ mod tests {
|
||||
let events = collect_move_events(&mut app);
|
||||
assert_eq!(events.len(), 1, "exactly one MoveRequestEvent expected");
|
||||
let evt = &events[0];
|
||||
assert_eq!(evt.from, PileType::Tableau(0));
|
||||
assert_eq!(evt.from, KlondikePile::Tableau(Tableau::Tableau1));
|
||||
assert_eq!(evt.to, dest_pile);
|
||||
assert_eq!(evt.count, 1);
|
||||
// State must return to Idle.
|
||||
@@ -1007,7 +1039,7 @@ mod tests {
|
||||
let mut app = radial_test_app();
|
||||
let layout_window = Vec2::new(1280.0, 800.0);
|
||||
let layout = compute_layout(layout_window, 0.0, 0.0, true);
|
||||
let ace_pos = layout.pile_positions[&PileType::Tableau(0)];
|
||||
let ace_pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau1)];
|
||||
|
||||
install_resources(&mut app, ace_only_state(), layout_window, ace_pos);
|
||||
press(&mut app, MouseButton::Right);
|
||||
@@ -1038,7 +1070,7 @@ mod tests {
|
||||
let mut app = radial_test_app();
|
||||
let layout_window = Vec2::new(1280.0, 800.0);
|
||||
let layout = compute_layout(layout_window, 0.0, 0.0, true);
|
||||
let ace_pos = layout.pile_positions[&PileType::Tableau(0)];
|
||||
let ace_pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau1)];
|
||||
|
||||
install_resources(&mut app, ace_only_state(), layout_window, ace_pos);
|
||||
press(&mut app, MouseButton::Right);
|
||||
@@ -1064,7 +1096,7 @@ mod tests {
|
||||
let mut app = radial_test_app();
|
||||
let layout_window = Vec2::new(1280.0, 800.0);
|
||||
let layout = compute_layout(layout_window, 0.0, 0.0, true);
|
||||
let king_pos = layout.pile_positions[&PileType::Tableau(0)];
|
||||
let king_pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau1)];
|
||||
|
||||
install_resources(&mut app, face_down_only_state(), layout_window, king_pos);
|
||||
press(&mut app, MouseButton::Right);
|
||||
|
||||
Reference in New Issue
Block a user