From c21c0ebf9917e6dbde806c4fcb7ab7546e36b60a Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 28 May 2026 16:23:05 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20revise=20integration=20plan=20=E2=80=94?= =?UTF-8?q?=20all=20gaps=20closed=20in=20Ferrous=20Solitaire=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframe the integration approach: klondike is a read-only dependency; all 8 gaps (scoring, game modes, solver, take_from_foundation, serde, MoveError, waste pile, undo stack) are closed in solitaire_core via a KlondikeAdapter wrapper layer. No upstream changes to card_game or klondike are required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/card-game-integration.md | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/card-game-integration.md b/docs/card-game-integration.md index 9852269..e21b4b5 100644 --- a/docs/card-game-integration.md +++ b/docs/card-game-integration.md @@ -1,6 +1,8 @@ # 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, and lists the concrete work needed before the two can be integrated. +**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. --- @@ -54,6 +56,8 @@ Ferrous uses Windows XP Standard scoring. `KlondikeStats` tracks a score and a r | 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: @@ -63,20 +67,22 @@ Ferrous has three modes that alter rules and scoring. None exist in `klondike` t | `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. -**Work needed:** Port or reimplement the DFS solver against `KlondikeState`. The memoisation key must be a deterministic canonical hash of the full game state. +**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. -**Work needed:** Add `foundation_to_tableau` as a valid `KlondikeInstruction` (guarded by a config flag). +**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`. -**Work needed:** Derive (or implement) `serde::Serialize` / `Deserialize` on `KlondikeState`, `KlondikeStats`, `KlondikeConfig`, and `SessionState`. The schema needs a version field so save files can be migrated. +**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.): @@ -90,32 +96,32 @@ InvalidDestination RuleViolation(String) ``` -`klondike::is_instruction_valid` returns `bool`. **Work needed:** Return `Result<(), MoveError>` (or an equivalent) so callers know *why* a move was rejected. +`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. -**Options:** -- Accept the replay cost (simple, no changes needed) -- Add a snapshot-based fast-undo path to `Session` +**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 (Suggested Steps) +## Integration Path (All work in `solitaire_core`) -Work items in rough dependency order: +Steps in dependency order — no changes required in `card_game` or `klondike`: -1. **Add `serde` feature** to `card_game` and `klondike` (gated, not mandatory) — unblocks persistence. -2. **Add flip-bonus + recycle-penalty scoring** to `KlondikeStats::process_instruction`. -3. **Add `GameMode`** config variant; gate undo and scoring accordingly. -4. **Add `MoveError` return type** to `process_instruction` / `is_instruction_valid`. -5. **Add `take_from_foundation` instruction** behind a config flag. -6. **Implement the DFS solvability solver** (can be a separate crate, e.g. `klondike-solver`). -7. **Add save/load round-trip tests** once serde is wired up. -8. **Adapter layer in `solitaire_core`** (or replace it) that wraps `klondike` types in the engine's `PileType`/`GameState` API so the Bevy engine layer (`solitaire_engine`) does not need to change. +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). ---