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:
+32
-18
@@ -87,18 +87,31 @@ pub struct CardSnapshot {
|
||||
pub face_up: bool,
|
||||
}
|
||||
|
||||
impl From<&solitaire_core::card::Card> for CardSnapshot {
|
||||
fn from(c: &solitaire_core::card::Card) -> Self {
|
||||
/// Stable 0..=51 identifier derived from suit and rank. Mirrors the desktop
|
||||
/// engine's `card_to_id` so replay snapshots are identical across platforms —
|
||||
/// `Card` itself carries no id field (suit + rank are unique within a deck).
|
||||
fn card_to_id(card: &solitaire_core::card::Card) -> u32 {
|
||||
let suit_index = match card.suit() {
|
||||
Suit::Clubs => 0,
|
||||
Suit::Diamonds => 1,
|
||||
Suit::Hearts => 2,
|
||||
Suit::Spades => 3,
|
||||
};
|
||||
suit_index * 13 + (card.rank().value() as u32 - 1)
|
||||
}
|
||||
|
||||
impl From<&(solitaire_core::card::Card, bool)> for CardSnapshot {
|
||||
fn from((card, face_up): &(solitaire_core::card::Card, bool)) -> Self {
|
||||
Self {
|
||||
id: c.id,
|
||||
suit: match c.suit {
|
||||
id: card_to_id(card),
|
||||
suit: match card.suit() {
|
||||
Suit::Clubs => "clubs",
|
||||
Suit::Diamonds => "diamonds",
|
||||
Suit::Hearts => "hearts",
|
||||
Suit::Spades => "spades",
|
||||
},
|
||||
rank: c.rank.value(),
|
||||
face_up: c.face_up,
|
||||
rank: card.rank().value(),
|
||||
face_up: *face_up,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,16 +402,17 @@ fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> Deb
|
||||
let mut out_of_range_card_ids = Vec::new();
|
||||
let mut total_cards_seen = 0_usize;
|
||||
|
||||
let mut feed = |cards: &[solitaire_core::card::Card]| {
|
||||
for card in cards {
|
||||
let mut feed = |cards: &[(solitaire_core::card::Card, bool)]| {
|
||||
for (card, _) in cards {
|
||||
total_cards_seen += 1;
|
||||
if card.id >= 52 {
|
||||
out_of_range_card_ids.push(card.id);
|
||||
let id = card_to_id(card);
|
||||
if id >= 52 {
|
||||
out_of_range_card_ids.push(id);
|
||||
continue;
|
||||
}
|
||||
let idx = card.id as usize;
|
||||
let idx = id as usize;
|
||||
if seen[idx] {
|
||||
duplicate_card_ids.push(card.id);
|
||||
duplicate_card_ids.push(id);
|
||||
} else {
|
||||
seen[idx] = true;
|
||||
}
|
||||
@@ -418,16 +432,16 @@ fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> Deb
|
||||
.filter(|id| !seen[*id as usize])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let stock_has_face_up_cards = stock.iter().any(|c| c.face_up);
|
||||
let waste_has_face_down_cards = waste.iter().any(|c| !c.face_up);
|
||||
let stock_has_face_up_cards = stock.iter().any(|(_, face_up)| *face_up);
|
||||
let waste_has_face_down_cards = waste.iter().any(|(_, face_up)| !*face_up);
|
||||
let foundation_has_face_down_cards = foundations
|
||||
.iter()
|
||||
.any(|pile| pile.iter().any(|c| !c.face_up));
|
||||
.any(|pile| pile.iter().any(|(_, face_up)| !*face_up));
|
||||
|
||||
let tableau_visibility_violation = tableaus.iter().any(|pile| {
|
||||
let mut seen_face_up = false;
|
||||
for card in pile {
|
||||
if card.face_up {
|
||||
for (_, face_up) in pile {
|
||||
if *face_up {
|
||||
seen_face_up = true;
|
||||
} else if seen_face_up {
|
||||
return true;
|
||||
@@ -599,7 +613,7 @@ impl SolitaireGame {
|
||||
.pile(KlondikePile::Tableau(tableau))
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|card| card.face_up)
|
||||
.take_while(|(_, face_up)| *face_up)
|
||||
.count();
|
||||
let skip = tableau_stack.skip_cards.0 as usize;
|
||||
let count = face_up_count.checked_sub(skip).ok_or_else(|| {
|
||||
|
||||
Reference in New Issue
Block a user