refactor(core): complete card_game::Card migration across engine + wasm
Finish the half-applied Card refactor. solitaire_core::card::Card is now an alias for the opaque card_game::Card: suit()/rank() are methods, there is no id or face_up field, and it is Clone+Eq+Hash but not Copy. Pile accessors return Vec<(Card, bool)> where the bool is face-up. Card identity is now the Card value itself (via Eq/Hash), not a numeric u32: - CardEntity stores `card: Card` (was `card_id: u32`); lookups compare cards. - Drag/selection collections and the touch/keyboard selection setters use Vec<Card>; CardFlippedEvent/CardFaceRevealedEvent/HintVisualEvent carry Card. - replay_overlay and feedback/settle/deal animations updated accordingly. solitaire_wasm: CardSnapshot derives its JSON id from suit+rank (matching the desktop engine), and consumes the (Card, bool) pile tuples. test-support: TestPileState tableau overrides now carry a per-card face-up flag so tests can place face-down tableau cards. set_test_tableau_cards keeps its Vec<Card> signature (defaulting to face-up); new set_test_tableau_cards_with_face takes Vec<(Card, bool)>. cargo test --workspace passes (engine lib 897 ok, 0 failed); cargo clippy --workspace --all-targets -- -D warnings is clean. Save/serde format unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -304,7 +304,7 @@ pub fn find_top_face_up_card_at(
|
||||
let is_tableau = matches!(pile, KlondikePile::Tableau(_));
|
||||
for i in (0..pile_cards.len()).rev() {
|
||||
let card = &pile_cards[i];
|
||||
if !card.face_up {
|
||||
if !card.1 {
|
||||
continue;
|
||||
}
|
||||
// Only the top card is draggable on non-tableau piles.
|
||||
@@ -320,7 +320,7 @@ pub fn find_top_face_up_card_at(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Some((pile, card.clone()));
|
||||
return Some((pile, card.0.clone()));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -339,7 +339,7 @@ fn card_position(
|
||||
if matches!(pile, KlondikePile::Tableau(_)) {
|
||||
let mut y_offset = 0.0_f32;
|
||||
for card in pile_cards(game, pile).iter().take(stack_index) {
|
||||
let step = if card.face_up {
|
||||
let step = if card.1 {
|
||||
TABLEAU_FAN_FRAC
|
||||
} else {
|
||||
TABLEAU_FACEDOWN_FAN_FRAC
|
||||
@@ -352,13 +352,27 @@ fn card_position(
|
||||
}
|
||||
}
|
||||
|
||||
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<Card> {
|
||||
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
|
||||
match pile {
|
||||
KlondikePile::Stock => game.waste_cards(),
|
||||
_ => game.pile(*pile),
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a `card_game::Card` to a stable `u32` identity used by `CardEntity`
|
||||
/// and systems that still track cards by numeric ID.
|
||||
/// Encoding: `suit_index * 13 + (rank.value() - 1)`, range 0..=51.
|
||||
fn card_to_id(card: &Card) -> u32 {
|
||||
use solitaire_core::card::Suit;
|
||||
let suit_index: u32 = match card.suit() {
|
||||
Suit::Clubs => 0,
|
||||
Suit::Diamonds => 1,
|
||||
Suit::Hearts => 2,
|
||||
Suit::Spades => 3,
|
||||
};
|
||||
suit_index * 13 + (card.rank().value() as u32 - 1)
|
||||
}
|
||||
|
||||
const fn foundations() -> [Foundation; 4] {
|
||||
[
|
||||
Foundation::Foundation1,
|
||||
@@ -498,7 +512,7 @@ fn radial_open_on_right_click(
|
||||
*state = RightClickRadialState::Active {
|
||||
source_pile,
|
||||
count: 1,
|
||||
cards: vec![card.id],
|
||||
cards: vec![card_to_id(&card)],
|
||||
legal_destinations,
|
||||
centre: world,
|
||||
hovered_index: None,
|
||||
@@ -571,7 +585,7 @@ fn radial_open_on_long_press(
|
||||
*state = RightClickRadialState::Active {
|
||||
source_pile,
|
||||
count: 1,
|
||||
cards: vec![card.id],
|
||||
cards: vec![card_to_id(&card)],
|
||||
legal_destinations,
|
||||
centre: world,
|
||||
hovered_index: None,
|
||||
@@ -794,7 +808,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::layout::compute_layout;
|
||||
use bevy::ecs::message::Messages;
|
||||
use solitaire_core::card::{Card as CoreCard, Rank, Suit};
|
||||
use solitaire_core::card::{Card as CoreCard, Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
|
||||
/// Build a minimal Bevy app wired with `RadialMenuPlugin` and the
|
||||
@@ -844,12 +858,7 @@ mod tests {
|
||||
// Ace of Clubs on Tableau(0).
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
}],
|
||||
vec![CoreCard::new(Deck::Deck1, Suit::Clubs, Rank::Ace)],
|
||||
);
|
||||
g
|
||||
}
|
||||
@@ -879,14 +888,9 @@ mod tests {
|
||||
] {
|
||||
g.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
g.set_test_tableau_cards(
|
||||
g.set_test_tableau_cards_with_face(
|
||||
Tableau::Tableau1,
|
||||
vec![CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::King,
|
||||
face_up: false,
|
||||
}],
|
||||
vec![(CoreCard::new(Deck::Deck1, Suit::Spades, Rank::King), false)],
|
||||
);
|
||||
g
|
||||
}
|
||||
@@ -979,12 +983,7 @@ mod tests {
|
||||
#[test]
|
||||
fn legal_destinations_for_ace_includes_only_first_empty_foundation() {
|
||||
let g = ace_only_state();
|
||||
let card = CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
};
|
||||
let card = CoreCard::new(Deck::Deck1, Suit::Clubs, Rank::Ace);
|
||||
let dests =
|
||||
legal_destinations_for_card(&card, &KlondikePile::Tableau(Tableau::Tableau1), &g);
|
||||
// Ace can be placed on every empty foundation. We only need
|
||||
@@ -999,12 +998,7 @@ mod tests {
|
||||
#[test]
|
||||
fn legal_destinations_excludes_source_pile() {
|
||||
let g = ace_only_state();
|
||||
let card = CoreCard {
|
||||
id: 100,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
};
|
||||
let card = CoreCard::new(Deck::Deck1, Suit::Clubs, Rank::Ace);
|
||||
let dests = legal_destinations_for_card(
|
||||
&card,
|
||||
&KlondikePile::Foundation(Foundation::Foundation1),
|
||||
|
||||
Reference in New Issue
Block a user