refactor(core): complete card_game::Card migration across engine + wasm
Build and Deploy / build-and-push (push) Failing after 1m2s
Web E2E / web-e2e (push) Failing after 3m19s

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:
funman300
2026-06-09 17:45:34 -07:00
parent 920f2c8597
commit 1438fd6265
22 changed files with 549 additions and 922 deletions
+32 -18
View File
@@ -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(|| {