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:
+1
-110
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user