# Integrating `card_game` / `klondike` as the Solitaire Core **Context:** A collaborator ([Quaternions](https://git.aleshym.co/Quaternions/card_game)) is building a pure-logic Klondike library in Rust. This document maps what that library currently provides against what Ferrous Solitaire's `solitaire_core` crate requires. **Approach:** `klondike` is treated as a **read-only dependency**. All gaps are closed in Ferrous Solitaire's own `solitaire_core` crate via a wrapper/adapter layer. No upstream changes to `card_game` or `klondike` are required — Quaternions can evolve their library independently. --- ## What `card_game` + `klondike` Already Has ### `card_game` crate (generic primitives) | Feature | Notes | |---|---| | `Card` (Deck + Suit + Rank packed in 1 byte) | `NonZeroU8` layout — no heap allocation | | `Suit`, `Rank`, `Deck` enums | Full A→K, 4 suits, up to 4 deck IDs | | `Stack` | Const-generic `ArrayVec` wrapper | | `Pile` | Face-down + face-up stacks; `flip_up`, `pop_flip_up` | | `Game` trait | `possible_instructions`, `is_instruction_valid`, `process_instruction`, `is_win` | | `Session` | Wraps a `Game`; tracks history, replays for undo | | `SessionInstruction::Undo` | Undo via full-state replay from seed | | `no_std`-compatible design | Const generics on stack sizes | ### `klondike` crate (Klondike rules) | Feature | Notes | |---|---| | 7 tableau + 4 foundation + 1 stock | Fully dealt from a seeded RNG | | Draw-1 / Draw-3 config | `KlondikeConfig::draw_stock` | | Foundation placement (Ace start, suit-matched A→K) | ✅ | | Tableau placement (alternating colour, K on empty) | ✅ | | Multi-card stack moves (via `SkipCards`) | ✅ | | `RotateStock` (recycle waste → stock) | ✅ | | `KlondikeStats` (score, recycle_count, moves) | Basic tracking | | `is_win_trivial` (all face-down cards cleared) | Auto-complete trigger | | `get_auto_move` / `get_sorted_moves` | Priority-ranked move suggestion | | Benchmark suite (`klondike-bench`) | 1 000-game throughput test | | CLI display (`klondike-cli`) | Terminal renderer | --- ## What Ferrous Solitaire's `solitaire_core` Needs (Gaps) The items below are either missing from `klondike` today or behave differently from what the engine expects. ### 1. Scoring — missing sub-rules Ferrous uses Windows XP Standard scoring. `KlondikeStats` tracks a score and a recycle count, but the following deltas are not yet applied: | Rule | `klondike` | `solitaire_core` | |---|---|---| | Card → foundation | +10 ✅ | +10 | | Waste → tableau | +5 ✅ | +5 | | Flip face-down card | ❌ | +5 | | Foundation → tableau | ❌ | −15 | | Undo | ❌ | −15 | | Recycle penalty (Draw-1) | ❌ | −100 after 1st free recycle | | Recycle penalty (Draw-3) | ❌ | −20 after 3rd free recycle | | Time bonus on win | ❌ | `700 000 / elapsed_seconds` | | Score floor of 0 | ❌ | `score.max(0)` enforced | **In our wrapper:** Implement all missing scoring deltas on top of `KlondikeState` in `solitaire_core`. Track flip bonus, undo penalty, and recycle penalties in our own adapter rather than inside `KlondikeStats`. ### 2. Game Modes Ferrous has three modes that alter rules and scoring. None exist in `klondike` today: | Mode | Behaviour | |---|---| | `Classic` | Standard scoring, undo allowed | | `Challenge` | Undo disabled (returns an error) | | `Zen` | All scoring disabled; score is always 0 | **In our wrapper:** Add `GameMode` to `solitaire_core::GameState`; intercept undo calls and scoring deltas in the adapter before delegating to `KlondikeState`. ### 3. Solvability Solver The engine has a Settings toggle — "Winnable deals only" — backed by a DFS solver with canonical-state memoisation (`solitaire_core::solver`). The solver classifies each deal as `Winnable`, `Unwinnable`, or `Inconclusive` (budget exceeded). `klondike` has no equivalent. **In our wrapper:** Port the existing DFS solver to run against `KlondikeState`. The memoisation key must be a deterministic canonical hash of the full game state. ### 4. `take_from_foundation` House Rule A configurable option allows moving the top foundation card back onto a compatible tableau column. `klondike` marks `Foundation → Foundation` as `is_useless` and does not expose foundation → tableau as a valid instruction. **In our wrapper:** Intercept and validate `foundation_to_tableau` moves in `solitaire_core` before delegating to `klondike`; guard with a config flag. ### 5. JSON Serialisation / Persistence `solitaire_core::GameState` serialises the full mid-game state to JSON via `serde` so the engine can save on exit and restore on launch. `KlondikeState` derives `Clone` + `Eq` + `Hash` but not `Serialize` / `Deserialize`. **In our wrapper:** Serialise the `solitaire_core` wrapper struct (which owns `KlondikeState` by value). Reconstruct `KlondikeState` from the seed + move history on load, or snapshot it as an opaque field. Schema version field lives on our wrapper. ### 6. Typed Move Errors `solitaire_core::error::MoveError` returns structured errors the engine uses to trigger UI feedback (wrong-destination toast, stock-empty chime, etc.): ``` GameAlreadyWon UndoStackEmpty StockEmpty InvalidSource InvalidDestination RuleViolation(String) ``` `klondike::is_instruction_valid` returns `bool`. **In our wrapper:** Call `is_instruction_valid` first; map `false` to the appropriate `MoveError` variant before returning to the engine. ### 7. Waste Pile as Separate Concept Ferrous tracks `PileType::Waste` as a distinct pile. `klondike` folds waste into `Stock` (the face-up half of the stock `Pile`). The engine's UI and scoring logic reference the waste pile directly; the mapping needs to be explicit. **In our wrapper:** Project the face-up half of `klondike`'s stock `Pile` as `PileType::Waste` when building pile snapshots for the engine. ### 8. Undo Stack Approach `solitaire_core` keeps a **snapshot stack** (up to 64 snapshots) so undo is O(1) without replaying history. `Session` undoes by replaying all moves from the seed, which is O(n). For a game-in-progress with many moves this is fine for a solver but may be perceptible in the UI on slow hardware. **In our wrapper:** Maintain our own snapshot ring buffer; bypass `Session::undo` and restore directly from the snapshot. Accept the replay cost only if profiling shows it is not perceptible. --- ## Integration Path (All work in `solitaire_core`) Steps in dependency order — no changes required in `card_game` or `klondike`: 1. **Add `klondike` as a dependency** of `solitaire_core`; stub out a `KlondikeAdapter` struct that wraps `KlondikeState` and passes all existing tests. 2. **Map pile types** — project `klondike`'s stock face-up half as `PileType::Waste`; expose the same `HashMap` the engine already reads (gap 7). 3. **Wrap move errors** — translate `klondike`'s `bool` returns to `Result<(), MoveError>` (gap 6). 4. **Port scoring** — implement flip-bonus, undo penalty, recycle penalty, time bonus, and score floor in the adapter (gap 1). 5. **Port `GameMode`** — intercept undo + scoring in the adapter based on mode (gap 2). 6. **Port `take_from_foundation`** — validate and apply in the adapter behind a config flag (gap 4). 7. **Port the DFS solver** to run against `KlondikeAdapter` (gap 3). 8. **Implement `serde`** on the adapter wrapper; migrate save-file schema (gap 5). --- ## What Does NOT Need to Change - The `solitaire_engine` Bevy layer — it works against `solitaire_core` types; changes are isolated to `solitaire_core`. - The `solitaire_sync` merge logic — operates on a `SyncPayload` DTO, independent of core card types. - The `solitaire_server` — speaks only `SyncPayload` JSON, unaffected. --- ## References - Friend's repo: - `solitaire_core` source: `solitaire_core/src/` - Scoring spec: `solitaire_core/src/scoring.rs` - Solver spec: `solitaire_core/src/solver.rs` - Architecture overview: `ARCHITECTURE.md`