- Approach: note Quaternions is addressing gaps 1 and 4 upstream (card_game issues #10 and #11) - Gap 1: replace comparison table with exact WXP scoring table from solitaire_core/src/scoring.rs; add solitaireparadise.com reference; note time bonus stays in adapter (not wasm-portable) - Gap 2: expand mode table with full Scoring + Undo columns; add descriptions for Zen (relaxed, score = 0) and Challenge (timed daily puzzle, undo disabled) - Gap 4: clarify the flag *enables* an optional move (off by default), not disables; link upstream issue #11 - Gap 5: note Quaternions confirmed newtypes approach, no upstream changes needed - Gap 6: document that MoveError is generated at instruction- construction boundary in solitaire_core, not by wrapping is_instruction_valid's bool - Gap 8: mark resolved; 0.02 ms worst case at 1M moves/s; drop snapshot ring-buffer plan - Integration path: updated steps to reflect resolved gaps and upstream issue dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Integrating card_game / klondike as the Solitaire Core
Context: A collaborator (Quaternions) 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: Most gaps are closed in Ferrous Solitaire's own solitaire_core crate via a wrapper/adapter layer. Two gaps (scoring sub-rules, take_from_foundation) are being addressed upstream by Quaternions and will be pulled in once merged. Quaternions confirmed as of the PR discussion that the latest version of card_game + klondike already supports the scoring specifics and game-mode configurability needed — integration is ready to begin.
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<CAP> |
Const-generic ArrayVec wrapper |
Pile<DN, UP> |
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. The exact table already implemented in solitaire_core/src/scoring.rs:
| Event | Delta |
|---|---|
| Any card → foundation | +10 |
| Waste → tableau | +5 |
| Flip face-down tableau card | +5 |
| Foundation → tableau | −15 |
| Undo | −15 |
| Recycle (Draw-1, after 1st free) | −100 |
| Recycle (Draw-3, after 3rd free) | −20 |
| Score floor | score.max(0) always |
| Time bonus on win | 700_000 / elapsed_seconds |
Reference: https://www.solitaireparadise.com/games_list/klondike_solitaire_scoring.html
KlondikeStats already tracks score and recycle count; the flip bonus, undo penalty, recycle penalties, score floor, and time bonus are not yet applied upstream. Quaternions has opened card_game issue #10 ✅ to address the missing scoring deltas.
Time bonus exception: The time bonus (700_000 / elapsed_seconds) depends on wall-clock time which is not wasm-portable. It stays tracked in solitaire_core's adapter regardless of upstream changes.
In our wrapper: Implement the time bonus and score floor in the adapter. Once upstream issue #10 lands, the remaining scoring deltas will be configurable from KlondikeStats directly.
2. Game Modes
Ferrous has three modes that alter scoring and undo behaviour:
| Mode | Scoring | Undo |
|---|---|---|
| Classic | Full WXP scoring (table above) | Allowed (−15 penalty) |
| Zen | All deltas suppressed — score stays 0 | Allowed (no penalty) |
| Challenge | Full WXP scoring | Disabled — returns an error |
Zen is intended for relaxed play where the score does not matter. Challenge is a timed daily puzzle where the no-undo constraint is the difficulty mechanic.
In our wrapper: Add GameMode to solitaire_core::GameState; intercept undo calls and scoring deltas in the adapter before delegating to KlondikeState. Quaternions confirmed the latest card_game + klondike supports the configurability required for all three modes.
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 enables an optional move that is off by default: moving the top card of a foundation pile back down onto a compatible tableau column. This is a house-rule relaxation that makes stuck games more recoverable; the standard rule disallows it. klondike marks Foundation → Foundation as is_useless and does not expose foundation → tableau as a valid instruction.
Quaternions has opened card_game issue #11 ✅ to add this as a config option.
In our wrapper: Guard the move with a config flag. Once upstream issue #11 lands, delegate to klondike's config; until then, intercept and validate in solitaire_core before delegating.
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. No upstream changes are needed here — Quaternions confirmed this is cleanly handled externally.
In our wrapper: Serialise the solitaire_core wrapper struct (which owns KlondikeState by value) using newtypes. 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. However, because KlondikeInstruction is always constructed by game code from valid entity layout, the only real failure mode is that solitaire_core fails to construct the instruction in the first place — the error lives at our construction boundary, not inside klondike.
In our wrapper: MoveError variants are generated when solitaire_core fails to construct a KlondikeInstruction from the player's requested move. No translation of is_instruction_valid's bool return is required; by the time an instruction reaches klondike, it is already known to be structurally valid.
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 (resolved — not an issue)
solitaire_core previously kept a snapshot stack so undo was O(1). Session undoes by replaying all moves from the seed — O(n). After discussion, this is not an issue: the benchmark shows 1 000 000 moves/second throughput, and a maximum game length of ~192 moves gives a worst-case undo time of 0.02 ms — imperceptible on any hardware.
Resolution: Use Session::undo as-is. The snapshot ring-buffer plan is dropped. No wrapper work needed for this item.
Integration Path (All work in solitaire_core)
Steps in dependency order. Upstream issues #10 and #11 can land at any time and will shrink the wrapper surface further.
- Add
klondikeas a dependency ofsolitaire_core; stub out aKlondikeAdapterstruct that wrapsKlondikeStateand passes all existing tests. - Map pile types — project
klondike's stock face-up half asPileType::Waste; expose the sameHashMap<PileType, Pile>the engine already reads (gap 7). - Port scoring — implement time bonus and score floor in the adapter; wire up the remaining WXP deltas (flip bonus, undo/recycle penalties) either from upstream (post issue #10) or directly in the adapter (gap 1).
- Port
GameMode— intercept undo + scoring in the adapter based on mode (gap 2). - Port
take_from_foundation— validate and apply in the adapter behind a config flag; delegate toklondike's config once upstream issue #11 lands (gap 4). - Port the DFS solver to run against
KlondikeAdapter(gap 3). - Implement
serdeon the adapter wrapper using newtypes; migrate save-file schema (gap 5).
What Does NOT Need to Change
- The
solitaire_engineBevy layer — it works againstsolitaire_coretypes; changes are isolated tosolitaire_core. - The
solitaire_syncmerge logic — operates on aSyncPayloadDTO, independent of core card types. - The
solitaire_server— speaks onlySyncPayloadJSON, unaffected.
References
- Quaternions' repo: https://git.aleshym.co/Quaternions/card_game
- Upstream issue — scoring: https://git.aleshym.co/Quaternions/card_game/issues/10
- Upstream issue — take_from_foundation: https://git.aleshym.co/Quaternions/card_game/issues/11
solitaire_coresource:solitaire_core/src/- Scoring spec:
solitaire_core/src/scoring.rs - Solver spec:
solitaire_core/src/solver.rs - Architecture overview:
ARCHITECTURE.md