Files
Ferrous-Solitaire/docs/card-game-integration.md
T
funman300 ccccdd2b40 docs: add card-game integration gap analysis
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>
2026-05-28 15:28:44 -07:00

137 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## 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:
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.
---
## 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: <https://git.aleshym.co/Quaternions/card_game>
- `solitaire_core` source: `solitaire_core/src/`
- Scoring spec: `solitaire_core/src/scoring.rs`
- Solver spec: `solitaire_core/src/solver.rs`
- Architecture overview: `ARCHITECTURE.md`