//! Adapter bridging `solitaire_core` types to the upstream `klondike` crate. //! //! [`KlondikeAdapter`] is a pure helper namespace for: //! - building [`KlondikeConfig`] from Ferrous settings //! - translating between local and upstream types //! - applying Ferrous-specific scoring policy on top of upstream defaults //! //! All `From` / `TryFrom` conversions between `solitaire_core` product types and //! upstream `card_game` / `klondike` types live here so that the product modules //! (`card`, `pile`, etc.) remain free of upstream dependencies. use klondike::{ DrawStockConfig, DstFoundation, DstTableau, Foundation, KlondikeConfig, KlondikeInstruction, KlondikePile, KlondikePileStack, MoveFromFoundationConfig, ScoringConfig, SkipCards, Tableau, TableauStack, }; use serde::{Deserialize, Serialize}; /// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate. /// /// This type is intentionally zero-sized: it does not carry mutable runtime /// state, and exists only as a namespace for configuration, conversion, and /// scoring helpers. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct KlondikeAdapter; impl KlondikeAdapter { /// Build a [`KlondikeConfig`] from draw mode and foundation house-rule setting. pub fn config_for(draw_mode: DrawStockConfig, take_from_foundation: bool) -> KlondikeConfig { KlondikeConfig { draw_stock: draw_mode, move_from_foundation: if take_from_foundation { MoveFromFoundationConfig::Allowed } else { MoveFromFoundationConfig::Disallowed }, scoring: ScoringConfig::DEFAULT, } } } /// Convert a zero-based tableau index (0..=6) into [`Tableau`]. pub fn tableau_from_index(index: usize) -> Option { match index { 0 => Some(Tableau::Tableau1), 1 => Some(Tableau::Tableau2), 2 => Some(Tableau::Tableau3), 3 => Some(Tableau::Tableau4), 4 => Some(Tableau::Tableau5), 5 => Some(Tableau::Tableau6), 6 => Some(Tableau::Tableau7), _ => None, } } /// Convert a zero-based foundation slot (0..=3) into [`Foundation`]. pub fn foundation_from_slot(slot: u8) -> Option { match slot { 0 => Some(Foundation::Foundation1), 1 => Some(Foundation::Foundation2), 2 => Some(Foundation::Foundation3), 3 => Some(Foundation::Foundation4), _ => None, } } /// Convert a tableau skip count (0..=12) into [`SkipCards`]. pub fn skip_cards_from_count(skip: usize) -> Option { match skip { 0 => Some(SkipCards::Skip0), 1 => Some(SkipCards::Skip1), 2 => Some(SkipCards::Skip2), 3 => Some(SkipCards::Skip3), 4 => Some(SkipCards::Skip4), 5 => Some(SkipCards::Skip5), 6 => Some(SkipCards::Skip6), 7 => Some(SkipCards::Skip7), 8 => Some(SkipCards::Skip8), 9 => Some(SkipCards::Skip9), 10 => Some(SkipCards::Skip10), 11 => Some(SkipCards::Skip11), 12 => Some(SkipCards::Skip12), _ => None, } } // ── Legacy serde mirror types (kept for backward compatibility) ─────────────── // // These types were introduced when upstream `klondike` had no serde feature. // Mainline `klondike` now provides full serde support (with a hand-written // compact `KlondikeInstruction` impl), and `GameState` serialises // `saved_moves` directly as `Vec` (schema v4). // // The mirror types are retained for three reasons: // 1. Schema v3 migration: `AnyInstruction` in `game_state.rs` uses // `TryFrom for KlondikeInstruction` to parse old save // files with u8 indices and replay them. // 2. `solitaire_data::ReplayMove` uses `SavedKlondikePile` as its serde // type; changing it would break the on-disk replay format (schema v2). // 3. `solitaire_wasm` mirrors `ReplayMove` using the same types so that // replay JSON is cross-compatible between the desktop and browser builds. // // These types should not be used for new serialisation concerns. If the // ReplayMove format is ever bumped to a new schema, migrate those callers to // `KlondikePile` / `KlondikePileStack` and the types here can then be deleted. /// A `Serialize` + `Deserialize` mirror of [`klondike::Tableau`] (0 = Tableau1 … 6 = Tableau7). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedTableau(pub u8); /// A `Serialize` + `Deserialize` mirror of [`klondike::Foundation`] (0 = Foundation1 … 3 = Foundation4). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedFoundation(pub u8); /// A `Serialize` + `Deserialize` mirror of [`klondike::SkipCards`] (0 = Skip0 … 12 = Skip12). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedSkipCards(pub u8); /// A `Serialize` + `Deserialize` mirror of [`klondike::KlondikePile`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SavedKlondikePile { Tableau(SavedTableau), Stock, Foundation(SavedFoundation), } /// A `Serialize` + `Deserialize` mirror of [`klondike::TableauStack`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedTableauStack { pub tableau: SavedTableau, pub skip_cards: SavedSkipCards, } /// A `Serialize` + `Deserialize` mirror of [`klondike::KlondikePileStack`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SavedKlondikePileStack { Tableau(SavedTableauStack), Stock, Foundation(SavedFoundation), } /// A `Serialize` + `Deserialize` mirror of [`klondike::DstFoundation`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedDstFoundation { pub src: SavedKlondikePile, pub foundation: SavedFoundation, } /// A `Serialize` + `Deserialize` mirror of [`klondike::DstTableau`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SavedDstTableau { pub src: SavedKlondikePileStack, pub tableau: SavedTableau, } /// A `Serialize` + `Deserialize` mirror of [`klondike::KlondikeInstruction`]. /// /// Convert to/from the upstream type with: /// ```ignore /// let saved = SavedInstruction::from(instruction); /// let instruction = KlondikeInstruction::try_from(saved)?; /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SavedInstruction { DstFoundation(SavedDstFoundation), DstTableau(SavedDstTableau), RotateStock, } /// Error returned when a [`SavedInstruction`] contains an out-of-range numeric value /// and cannot be converted back to a [`klondike::KlondikeInstruction`]. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum InvalidSavedInstruction { #[error("invalid tableau index {0} (expected 0–6)")] Tableau(u8), #[error("invalid foundation index {0} (expected 0–3)")] Foundation(u8), #[error("invalid skip_cards value {0} (expected 0–12)")] SkipCards(u8), } // ── From impls: KlondikeInstruction → Saved* ───────────────────────────────── impl From for SavedTableau { fn from(t: Tableau) -> Self { Self(t as u8) } } impl From for SavedFoundation { fn from(f: Foundation) -> Self { Self(f as u8) } } impl From for SavedSkipCards { fn from(s: SkipCards) -> Self { Self(s as u8) } } impl From for SavedKlondikePile { fn from(p: KlondikePile) -> Self { match p { KlondikePile::Tableau(t) => Self::Tableau(t.into()), KlondikePile::Stock => Self::Stock, KlondikePile::Foundation(f) => Self::Foundation(f.into()), } } } impl From for SavedTableauStack { fn from(ts: TableauStack) -> Self { Self { tableau: ts.tableau.into(), skip_cards: ts.skip_cards.into(), } } } impl From for SavedKlondikePileStack { fn from(ps: KlondikePileStack) -> Self { match ps { KlondikePileStack::Tableau(ts) => Self::Tableau(ts.into()), KlondikePileStack::Stock => Self::Stock, KlondikePileStack::Foundation(f) => Self::Foundation(f.into()), } } } impl From for SavedDstFoundation { fn from(df: DstFoundation) -> Self { Self { src: df.src.into(), foundation: df.foundation.into(), } } } impl From for SavedDstTableau { fn from(dt: DstTableau) -> Self { Self { src: dt.src.into(), tableau: dt.tableau.into(), } } } impl From for SavedInstruction { fn from(i: KlondikeInstruction) -> Self { match i { KlondikeInstruction::RotateStock => Self::RotateStock, KlondikeInstruction::DstFoundation(df) => Self::DstFoundation(df.into()), KlondikeInstruction::DstTableau(dt) => Self::DstTableau(dt.into()), } } } // ── TryFrom impls: Saved* → KlondikeInstruction ────────────────────────────── impl TryFrom for Tableau { type Error = InvalidSavedInstruction; fn try_from(s: SavedTableau) -> Result { tableau_from_index(s.0 as usize).ok_or(InvalidSavedInstruction::Tableau(s.0)) } } impl TryFrom for Foundation { type Error = InvalidSavedInstruction; fn try_from(s: SavedFoundation) -> Result { foundation_from_slot(s.0).ok_or(InvalidSavedInstruction::Foundation(s.0)) } } impl TryFrom for SkipCards { type Error = InvalidSavedInstruction; fn try_from(s: SavedSkipCards) -> Result { skip_cards_from_count(s.0 as usize).ok_or(InvalidSavedInstruction::SkipCards(s.0)) } } impl TryFrom for KlondikePile { type Error = InvalidSavedInstruction; fn try_from(s: SavedKlondikePile) -> Result { Ok(match s { SavedKlondikePile::Tableau(t) => KlondikePile::Tableau(t.try_into()?), SavedKlondikePile::Stock => KlondikePile::Stock, SavedKlondikePile::Foundation(f) => KlondikePile::Foundation(f.try_into()?), }) } } impl TryFrom for TableauStack { type Error = InvalidSavedInstruction; fn try_from(s: SavedTableauStack) -> Result { Ok(TableauStack { tableau: s.tableau.try_into()?, skip_cards: s.skip_cards.try_into()?, }) } } impl TryFrom for KlondikePileStack { type Error = InvalidSavedInstruction; fn try_from(s: SavedKlondikePileStack) -> Result { Ok(match s { SavedKlondikePileStack::Tableau(ts) => KlondikePileStack::Tableau(ts.try_into()?), SavedKlondikePileStack::Stock => KlondikePileStack::Stock, SavedKlondikePileStack::Foundation(f) => KlondikePileStack::Foundation(f.try_into()?), }) } } impl TryFrom for DstFoundation { type Error = InvalidSavedInstruction; fn try_from(s: SavedDstFoundation) -> Result { Ok(DstFoundation { src: s.src.try_into()?, foundation: s.foundation.try_into()?, }) } } impl TryFrom for DstTableau { type Error = InvalidSavedInstruction; fn try_from(s: SavedDstTableau) -> Result { Ok(DstTableau { src: s.src.try_into()?, tableau: s.tableau.try_into()?, }) } } impl TryFrom for KlondikeInstruction { type Error = InvalidSavedInstruction; fn try_from(s: SavedInstruction) -> Result { Ok(match s { SavedInstruction::RotateStock => KlondikeInstruction::RotateStock, SavedInstruction::DstFoundation(df) => { KlondikeInstruction::DstFoundation(df.try_into()?) } SavedInstruction::DstTableau(dt) => KlondikeInstruction::DstTableau(dt.try_into()?), }) } } /// Time bonus added to the score on a win: `700_000 / elapsed_seconds`. /// Returns 0 when `elapsed_seconds` is 0 to avoid division by zero. pub fn compute_time_bonus(elapsed_seconds: u64) -> i32 { if elapsed_seconds == 0 { return 0; } (700_000u64 / elapsed_seconds).min(i32::MAX as u64) as i32 }