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
+1 -110
View File
@@ -1,110 +1 @@
use serde::{Deserialize, Serialize};
pub use card_game::{Rank, Suit};
/// A single playing card.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Card {
/// Unique identifier for this card within the deal. Stable across moves and undo.
pub id: u32,
/// The card's suit (Clubs, Diamonds, Hearts, Spades).
pub suit: Suit,
/// The card's rank (Ace through King).
pub rank: Rank,
/// Whether the card is visible to the player. Face-down cards may not be moved.
pub face_up: bool,
}
impl Card {
/// Creates a card with explicit face orientation.
pub const fn new(id: u32, suit: Suit, rank: Rank, face_up: bool) -> Self {
Self {
id,
suit,
rank,
face_up,
}
}
/// Creates a face-up card.
pub const fn face_up(id: u32, suit: Suit, rank: Rank) -> Self {
Self::new(id, suit, rank, true)
}
/// Creates a face-down card.
pub const fn face_down(id: u32, suit: Suit, rank: Rank) -> Self {
Self::new(id, suit, rank, false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_values_are_sequential() {
for (i, r) in Rank::RANKS.iter().enumerate() {
assert_eq!(r.value(), (i + 1) as u8);
}
}
#[test]
fn rank_as_u8_matches_value() {
for r in Rank::RANKS {
assert_eq!(r as u8, r.value());
}
}
#[test]
fn rank_checked_add_boundary() {
assert_eq!(Rank::King.checked_add(1), None);
assert_eq!(Rank::Queen.checked_add(1), Some(Rank::King));
assert_eq!(Rank::Ace.checked_add(1), Some(Rank::Two));
assert_eq!(Rank::Five.checked_add(3), Some(Rank::Eight));
}
#[test]
fn rank_checked_sub_boundary() {
assert_eq!(Rank::Ace.checked_sub(1), None);
assert_eq!(Rank::Two.checked_sub(1), Some(Rank::Ace));
assert_eq!(Rank::King.checked_sub(1), Some(Rank::Queen));
assert_eq!(Rank::Five.checked_sub(3), Some(Rank::Two));
}
#[test]
fn suit_suits_contains_all_four() {
assert_eq!(Suit::SUITS.len(), 4);
assert!(Suit::SUITS.contains(&Suit::Clubs));
assert!(Suit::SUITS.contains(&Suit::Diamonds));
assert!(Suit::SUITS.contains(&Suit::Hearts));
assert!(Suit::SUITS.contains(&Suit::Spades));
}
#[test]
fn suit_red_and_black_are_complementary() {
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
assert_ne!(
suit.is_red(),
suit.is_black(),
"{suit:?} must be exactly one of red/black"
);
}
assert!(Suit::Diamonds.is_red() && Suit::Hearts.is_red());
assert!(Suit::Clubs.is_black() && Suit::Spades.is_black());
}
#[test]
fn card_constructors_set_fields() {
let up = Card::face_up(10, Suit::Spades, Rank::Queen);
assert_eq!(up.id, 10);
assert_eq!(up.suit, Suit::Spades);
assert_eq!(up.rank, Rank::Queen);
assert!(up.face_up);
let down = Card::face_down(11, Suit::Diamonds, Rank::King);
assert_eq!(down.id, 11);
assert_eq!(down.suit, Suit::Diamonds);
assert_eq!(down.rank, Rank::King);
assert!(!down.face_up);
}
}
pub use card_game::{Card, Deck, Rank, Suit};