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,13 +1,12 @@
|
||||
use crate::card::Card;
|
||||
use crate::error::MoveError;
|
||||
use crate::klondike_adapter::{
|
||||
DrawMode, KlondikeAdapter, SavedInstruction, card_from_kl,
|
||||
DrawMode, KlondikeAdapter, SavedInstruction,
|
||||
compute_time_bonus as scoring_time_bonus,
|
||||
foundation_from_slot as adapter_foundation_from_slot,
|
||||
skip_cards_from_count as adapter_skip_cards_from_count,
|
||||
tableau_from_index as adapter_tableau_from_index,
|
||||
};
|
||||
use card_game::{Game as _, Session, SessionConfig};
|
||||
use card_game::{Card, Game as _, Session, SessionConfig};
|
||||
use klondike::{
|
||||
DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction,
|
||||
KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack,
|
||||
@@ -142,13 +141,15 @@ struct PersistedGameStateIn {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TestPileState {
|
||||
/// Override for face-down stock cards. `None` means "use session".
|
||||
pub stock: Option<Vec<crate::card::Card>>,
|
||||
pub stock: Option<Vec<Card>>,
|
||||
/// Override for face-up waste cards. `None` means "use session".
|
||||
pub waste: Option<Vec<crate::card::Card>>,
|
||||
pub waste: Option<Vec<Card>>,
|
||||
/// Per-tableau overrides. Missing keys fall back to the session.
|
||||
pub tableau: std::collections::HashMap<Tableau, Vec<crate::card::Card>>,
|
||||
/// Each entry carries its own face-up flag so tests can place face-down
|
||||
/// cards (e.g. an un-flipped tableau card).
|
||||
pub tableau: std::collections::HashMap<Tableau, Vec<(Card, bool)>>,
|
||||
/// Per-foundation overrides. Missing keys fall back to the session.
|
||||
pub foundation: std::collections::HashMap<Foundation, Vec<crate::card::Card>>,
|
||||
pub foundation: std::collections::HashMap<Foundation, Vec<Card>>,
|
||||
}
|
||||
|
||||
/// Full state of an in-progress Klondike Solitaire game.
|
||||
@@ -417,55 +418,52 @@ impl GameState {
|
||||
self.session.history().len()
|
||||
}
|
||||
|
||||
fn cards_with_face(cards: impl IntoIterator<Item = Card>, face_up: bool) -> Vec<Card> {
|
||||
cards
|
||||
.into_iter()
|
||||
.map(|mut card| {
|
||||
card.face_up = face_up;
|
||||
card
|
||||
})
|
||||
.collect()
|
||||
fn cards_with_face(
|
||||
cards: impl IntoIterator<Item = Card>,
|
||||
face_up: bool,
|
||||
) -> Vec<(Card, bool)> {
|
||||
cards.into_iter().map(|card| (card, face_up)).collect()
|
||||
}
|
||||
|
||||
pub fn stock_cards(&self) -> Vec<Card> {
|
||||
pub fn stock_cards(&self) -> Vec<(Card, bool)> {
|
||||
#[cfg(feature = "test-support")]
|
||||
if let Some(ref state) = self.test_pile_state
|
||||
&& let Some(ref cards) = state.stock
|
||||
{
|
||||
return cards.clone();
|
||||
return cards.iter().map(|c| (c.clone(), false)).collect();
|
||||
}
|
||||
let state = self.session.state().state().state();
|
||||
Self::cards_with_face(state.stock().face_down().iter().map(card_from_kl), false)
|
||||
Self::cards_with_face(state.stock().face_down().iter().cloned(), false)
|
||||
}
|
||||
|
||||
pub fn waste_cards(&self) -> Vec<Card> {
|
||||
pub fn waste_cards(&self) -> Vec<(Card, bool)> {
|
||||
#[cfg(feature = "test-support")]
|
||||
if let Some(ref state) = self.test_pile_state
|
||||
&& let Some(ref cards) = state.waste
|
||||
{
|
||||
return cards.clone();
|
||||
return cards.iter().map(|c| (c.clone(), true)).collect();
|
||||
}
|
||||
let state = self.session.state().state().state();
|
||||
Self::cards_with_face(state.stock().face_up().iter().map(card_from_kl), true)
|
||||
Self::cards_with_face(state.stock().face_up().iter().cloned(), true)
|
||||
}
|
||||
|
||||
/// Returns the cards in the requested pile.
|
||||
/// Returns the cards in the requested pile as `(card, face_up)` tuples.
|
||||
///
|
||||
/// **Note on `KlondikePile::Stock`:** this variant returns the face-up
|
||||
/// *waste* pile, not the face-down draw stack. Use [`Self::stock_cards`]
|
||||
/// to read the face-down draw cards.
|
||||
pub fn pile(&self, pile: KlondikePile) -> Vec<Card> {
|
||||
pub fn pile(&self, pile: KlondikePile) -> Vec<(Card, bool)> {
|
||||
#[cfg(feature = "test-support")]
|
||||
if let Some(ref state) = self.test_pile_state {
|
||||
match pile {
|
||||
KlondikePile::Stock => {
|
||||
if let Some(ref cards) = state.waste {
|
||||
return cards.clone();
|
||||
return cards.iter().map(|c| (c.clone(), true)).collect();
|
||||
}
|
||||
}
|
||||
KlondikePile::Foundation(f) => {
|
||||
if let Some(cards) = state.foundation.get(&f) {
|
||||
return cards.clone();
|
||||
return cards.iter().map(|c| (c.clone(), true)).collect();
|
||||
}
|
||||
}
|
||||
KlondikePile::Tableau(t) => {
|
||||
@@ -485,21 +483,13 @@ impl GameState {
|
||||
Foundation::Foundation3 => state.foundation3(),
|
||||
Foundation::Foundation4 => state.foundation4(),
|
||||
};
|
||||
Self::cards_with_face(cards.iter().map(card_from_kl), true)
|
||||
Self::cards_with_face(cards.iter().cloned(), true)
|
||||
}
|
||||
KlondikePile::Tableau(tableau) => {
|
||||
let mut cards = Self::cards_with_face(
|
||||
state
|
||||
.tableau_face_down_cards(tableau)
|
||||
.iter()
|
||||
.map(card_from_kl),
|
||||
false,
|
||||
);
|
||||
let mut cards =
|
||||
Self::cards_with_face(state.tableau_face_down_cards(tableau).iter().cloned(), false);
|
||||
cards.extend(Self::cards_with_face(
|
||||
state
|
||||
.tableau_face_up_cards(tableau)
|
||||
.iter()
|
||||
.map(card_from_kl),
|
||||
state.tableau_face_up_cards(tableau).iter().cloned(),
|
||||
true,
|
||||
));
|
||||
cards
|
||||
@@ -515,7 +505,7 @@ impl GameState {
|
||||
adapter_foundation_from_slot(slot).ok_or(MoveError::InvalidDestination)
|
||||
}
|
||||
|
||||
pub fn foundation_cards(&self, slot: u8) -> Result<Vec<Card>, MoveError> {
|
||||
pub fn foundation_cards(&self, slot: u8) -> Result<Vec<(Card, bool)>, MoveError> {
|
||||
let foundation = Self::foundation_from_slot(slot)?;
|
||||
Ok(self.pile(KlondikePile::Foundation(foundation)))
|
||||
}
|
||||
@@ -560,8 +550,20 @@ impl GameState {
|
||||
}
|
||||
|
||||
/// Test-support helper: override cards for a specific tableau column.
|
||||
///
|
||||
/// All provided cards are treated as face-up. Use
|
||||
/// [`Self::set_test_tableau_cards_with_face`] when a test needs to place
|
||||
/// face-down cards.
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn set_test_tableau_cards(&mut self, tableau: Tableau, cards: Vec<Card>) {
|
||||
let with_face = cards.into_iter().map(|c| (c, true)).collect();
|
||||
self.set_test_tableau_cards_with_face(tableau, with_face);
|
||||
}
|
||||
|
||||
/// Test-support helper: override cards for a specific tableau column,
|
||||
/// specifying each card's face-up flag (`true` = face-up).
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn set_test_tableau_cards_with_face(&mut self, tableau: Tableau, cards: Vec<(Card, bool)>) {
|
||||
let state = self
|
||||
.test_pile_state
|
||||
.get_or_insert_with(TestPileState::default);
|
||||
@@ -578,21 +580,15 @@ impl GameState {
|
||||
}
|
||||
|
||||
/// Test-support helper: override cards for a specific pile.
|
||||
///
|
||||
/// For `KlondikePile::Stock`, all provided cards go to the face-down stock
|
||||
/// override. Use [`Self::set_test_waste_cards`] to override the waste pile
|
||||
/// separately.
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn set_test_pile_cards(&mut self, pile: KlondikePile, cards: Vec<Card>) {
|
||||
match pile {
|
||||
KlondikePile::Stock => {
|
||||
let mut stock = Vec::new();
|
||||
let mut waste = Vec::new();
|
||||
for card in cards {
|
||||
if card.face_up {
|
||||
waste.push(card);
|
||||
} else {
|
||||
stock.push(card);
|
||||
}
|
||||
}
|
||||
self.set_test_stock_cards(stock);
|
||||
self.set_test_waste_cards(waste);
|
||||
self.set_test_stock_cards(cards);
|
||||
}
|
||||
KlondikePile::Tableau(t) => self.set_test_tableau_cards(t, cards),
|
||||
KlondikePile::Foundation(f) => self.set_test_foundation_cards(f, cards),
|
||||
@@ -612,7 +608,7 @@ impl GameState {
|
||||
if pile.is_empty() {
|
||||
return false;
|
||||
}
|
||||
pile.len() > count && !pile[pile.len() - count - 1].face_up
|
||||
pile.len() > count && !pile[pile.len() - count - 1].1
|
||||
}
|
||||
|
||||
/// Returns `(score_delta, is_recycle)` for `instruction` given the *current*
|
||||
@@ -962,24 +958,24 @@ impl GameState {
|
||||
.is_instruction_valid(&config, instruction)
|
||||
}
|
||||
|
||||
/// Returns the current pile containing `card_id`, if any.
|
||||
pub fn pile_containing_card(&self, card_id: u32) -> Option<KlondikePile> {
|
||||
if self.stock_cards().iter().any(|card| card.id == card_id)
|
||||
|| self.waste_cards().iter().any(|card| card.id == card_id)
|
||||
/// Returns the current pile containing `card`, if any.
|
||||
pub fn pile_containing_card(&self, card: Card) -> Option<KlondikePile> {
|
||||
if self.stock_cards().iter().any(|(c, _)| *c == card)
|
||||
|| self.waste_cards().iter().any(|(c, _)| *c == card)
|
||||
{
|
||||
return Some(KlondikePile::Stock);
|
||||
}
|
||||
for slot in 0..4_u8 {
|
||||
let foundation = Self::foundation_from_slot(slot).ok()?;
|
||||
let pile = self.pile(KlondikePile::Foundation(foundation));
|
||||
if pile.iter().any(|card| card.id == card_id) {
|
||||
if pile.iter().any(|(c, _)| *c == card) {
|
||||
return Some(KlondikePile::Foundation(foundation));
|
||||
}
|
||||
}
|
||||
for index in 0..7_usize {
|
||||
let tableau = Self::tableau_from_index(index).ok()?;
|
||||
let pile = self.pile(KlondikePile::Tableau(tableau));
|
||||
if pile.iter().any(|card| card.id == card_id) {
|
||||
if pile.iter().any(|(c, _)| *c == card) {
|
||||
return Some(KlondikePile::Tableau(tableau));
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1035,7 @@ mod tests {
|
||||
|
||||
for _ in 0..MAX_STEPS {
|
||||
let moves = game.possible_instructions();
|
||||
if let Some((from, to, _count)) = moves.iter().copied().find(|(from, to, count)| {
|
||||
if let Some((from, to, _count)) = moves.iter().cloned().find(|(from, to, count)| {
|
||||
*count == 1
|
||||
&& matches!(from, KlondikePile::Foundation(_))
|
||||
&& matches!(to, KlondikePile::Tableau(_))
|
||||
@@ -1047,7 +1043,7 @@ mod tests {
|
||||
return Some((game, from, to));
|
||||
}
|
||||
|
||||
if let Some((from, to, count)) = moves.iter().copied().find(|(from, to, count)| {
|
||||
if let Some((from, to, count)) = moves.iter().cloned().find(|(from, to, count)| {
|
||||
*count == 1
|
||||
&& !matches!(from, KlondikePile::Foundation(_))
|
||||
&& matches!(to, KlondikePile::Foundation(_))
|
||||
|
||||
Reference in New Issue
Block a user