Documents what Quaternions/card_game already provides, what solitaire_core requires that is currently missing, and the suggested step-by-step integration path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6.8 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, and lists the concrete work needed before the two can be integrated.
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. 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 |
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 |
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.
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).
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.
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. Work needed: Return Result<(), MoveError> (or an equivalent) so callers know why a move was rejected.
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.
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
Integration Path (Suggested Steps)
Work items in rough dependency order:
- Add
serdefeature tocard_gameandklondike(gated, not mandatory) — unblocks persistence. - Add flip-bonus + recycle-penalty scoring to
KlondikeStats::process_instruction. - Add
GameModeconfig variant; gate undo and scoring accordingly. - Add
MoveErrorreturn type toprocess_instruction/is_instruction_valid. - Add
take_from_foundationinstruction behind a config flag. - Implement the DFS solvability solver (can be a separate crate, e.g.
klondike-solver). - Add save/load round-trip tests once serde is wired up.
- Adapter layer in
solitaire_core(or replace it) that wrapsklondiketypes in the engine'sPileType/GameStateAPI so the Bevy engine layer (solitaire_engine) does not need to change.
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
- Friend's repo: https://git.aleshym.co/Quaternions/card_game
solitaire_coresource:solitaire_core/src/- Scoring spec:
solitaire_core/src/scoring.rs - Solver spec:
solitaire_core/src/solver.rs - Architecture overview:
ARCHITECTURE.md