diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 22b6989..9c274c5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,9 +1,11 @@ # Ferrous Solitaire — Architecture Document -> **Version:** 1.3 +> **Version:** 1.4 > **Language:** Rust (Edition 2024) > **Engine:** Bevy (latest stable) -> **Last Updated:** 2026-05-12 +> **Last Updated:** 2026-07-06 — post card_game/klondike migration (PR #88): +> core card/pile types come from the upstream `card_game` workspace and +> score/undo/recycle are derived from the upstream session, not stored. --- @@ -82,13 +84,15 @@ ferrous_solitaire/ │ ├── win_fanfare.wav │ └── ambient_loop.wav │ -├── solitaire_core/ # Pure Rust game logic — zero external deps beyond rand/serde +├── solitaire_core/ # Pure Rust game rules — wraps upstream card_game/klondike (serde + thiserror only otherwise) ├── solitaire_sync/ # Shared API types — used by client and server ├── solitaire_data/ # Persistence, sync client, settings ├── solitaire_engine/ # Bevy ECS systems, components, plugins -├── solitaire_server/ # Self-hosted sync server (Axum + SQLite) -├── solitaire_wasm/ # WebAssembly bindings — browser-side replay player -└── solitaire_app/ # Main binary entry point +├── solitaire_server/ # Self-hosted sync server (Axum + SQLite) + web frontend +├── solitaire_wasm/ # WebAssembly bindings — browser-side logic/replay + debug bridge +├── solitaire_web/ # Bevy WASM canvas build for the browser /play route +├── solitaire_assetgen/ # One-shot generator for card/background PNG assets +└── solitaire_app/ # Main binary entry point (desktop + Android cdylib) ``` --- @@ -96,18 +100,30 @@ ferrous_solitaire/ ## 3. Crate Responsibilities ### `solitaire_core` -**Dependencies:** `rand`, `serde`, `chrono` only. +**Dependencies:** `serde`, `thiserror`, plus the upstream `card_game` and +`klondike` crates (pinned via the Quaternions registry — never edit upstream). -The entire game rules engine. No Bevy, no network, no file I/O. Designed to be tested in isolation with `cargo test -p solitaire_core`. +The game rules layer. No Bevy, no network, no file I/O. Designed to be tested +in isolation with `cargo test -p solitaire_core`. + +Since the card_game migration (2026-06-22, PR #88) the primitive types are +**upstream**: `Card`, `Deck`, `Suit`, `Rank`, `Session` come from `card_game`; +`Klondike`, `KlondikePile`, `KlondikeInstruction`, `DrawStockConfig`, +`Foundation`, `Tableau` come from `klondike`. `solitaire_core` re-exports them +so downstream crates import from one place and never depend on the upstream +crates directly. Owns: -- All game data models (`Card`, `Suit`, `Rank`, `Pile`, `GameState`) -- Move validation logic -- Scoring engine -- Undo stack +- `GameState` — a wrapper around the upstream `Session`; the session + is the single source of truth for board state and stats +- `MoveError` and the `Result`-based mutation API +- `KlondikeConfig` adaptation (`klondike_adapter`) — draw mode, scoring, + take-from-foundation +- `GameMode` (Classic / Zen / Challenge / TimeAttack) and mode-aware scoring +- Solvability check API (`SolveOutcome`, delegating to `Session::solve`) - Win / auto-complete detection - Achievement unlock condition evaluation -- Seeded RNG for reproducible deals +- Seeded deals (same seed ⇒ same layout, via the upstream dealer) **Rules decisions:** - **Stock recycling is unlimited in every draw mode — by design.** Extra @@ -189,9 +205,13 @@ Owns: Because `ReplayPlayer` uses the same `solitaire_core::GameState` as the desktop client, the two implementations cannot drift: the same seed + move list produces identical pile state at every step on both platforms. ### `solitaire_app` -**Dependencies:** `bevy`, `solitaire_engine`. +**Dependencies:** `bevy`, `solitaire_engine`, `solitaire_data` (+ `jni` on Android). -Thin binary entry point. Registers all Bevy plugins and sets initial window properties. +Thin entry point (desktop binary + Android `cdylib`). Registers all Bevy +plugins and sets initial window properties. The one crate in the workspace +allowed `unsafe`: the Android entry point reconstructs the raw JNI handles and +hands them to the safe `solitaire_data::android_jni` bridge; everything else +is `forbid(unsafe_code)`. --- @@ -559,26 +579,32 @@ This ensures all players worldwide get the same challenge for a given date, rega ### Core Game Models (`solitaire_core`) +Since the card_game migration, the primitives are upstream types re-exported +through `solitaire_core`: + ```rust -pub enum Suit { Clubs, Diamonds, Hearts, Spades } -pub enum Rank { Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King } +// From `card_game` (upstream — never edit): +pub enum Suit { /* Clubs, Diamonds, Hearts, Spades */ } +pub enum Rank { /* Ace ..= King */ } +pub struct Card { /* deck + suit + rank; identity type, no face_up flag — + facing is positional, tracked by the Klondike board */ } +pub struct Session { /* replayable instruction log + derived stats */ } -pub struct Card { - pub id: u32, - pub suit: Suit, - pub rank: Rank, - pub face_up: bool, -} - -pub enum PileType { +// From `klondike` (upstream — never edit): +pub enum KlondikePile { Stock, Waste, - Foundation(Suit), - Tableau(usize), // 0–6 + Foundation(Foundation), // 4 slots, any suit may claim any slot + Tableau(Tableau), // 7 columns } +pub enum DrawStockConfig { DrawOne, DrawThree } +pub enum KlondikeInstruction { /* RotateStock, DstFoundation, ... — the + serialized move format (schema v4+) */ } +``` -pub enum DrawMode { DrawOne, DrawThree } +Owned by `solitaire_core`: +```rust /// Active game mode. Classic is the default; others unlock at level 5. pub enum GameMode { Classic, Zen, Challenge, TimeAttack } @@ -589,24 +615,30 @@ pub enum MoveError { RuleViolation(String), UndoStackEmpty, GameAlreadyWon, + StockEmpty, } pub struct GameState { - pub piles: HashMap>, - pub draw_mode: DrawMode, pub mode: GameMode, - pub score: i32, - pub move_count: u32, - pub undo_count: u32, // number of undos used in this game - pub recycle_count: u32, // number of stock recycles pub elapsed_seconds: u64, - pub seed: u64, - pub is_won: bool, - pub is_auto_completable: bool, - undo_stack: VecDeque, // private, max 64 (VecDeque for O(1) pop_front) + pub seed: u64, // same seed ⇒ same deal + pub take_from_foundation: bool, + session: Session, // private — the single source of truth } ``` +**Derived, not stored:** `score()`, `move_count()`, `undo_count()`, +`recycle_count()`, `is_won()`, `is_auto_completable()`, and all pile +accessors read through the session. Undo replays the instruction log +(no snapshot stack); the −15 undo penalty is applied by the upstream +score formula via the session config. Persistence (schema v5) saves +`saved_moves` as upstream `KlondikeInstruction`s and rebuilds the +session by replay on load — older files carrying `score`/`undo_count`/ +`recycle_count` keys load fine, the extra fields are ignored. + +**Rules decision:** stock recycling is unlimited in every draw mode +(see the "Rules decisions" note in §3 `solitaire_core`). + ### Persistence Models (`solitaire_data`) ```rust @@ -644,7 +676,7 @@ pub struct AchievementRecord { } pub struct Settings { - pub draw_mode: DrawMode, + pub draw_mode: DrawStockConfig, pub sfx_volume: f32, // 0.0–1.0 pub music_volume: f32, pub animation_speed: AnimSpeed, diff --git a/CLAUDE.md b/CLAUDE.md index 79323a8..71cb9ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,9 @@ solitaire_data/ # Persistence + sync client solitaire_engine/ # Bevy ECS + UI + gameplay orchestration solitaire_server/ # Axum backend (optional sync layer) solitaire_wasm/ # WASM bindings for browser-side replay player -solitaire_app/ # Entry binary +solitaire_web/ # Bevy WASM canvas build for the browser /play route +solitaire_assetgen/ # One-shot card/background PNG asset generator +solitaire_app/ # Entry binary (desktop + Android cdylib) assets/ # Runtime assets (except audio + default theme) ``` diff --git a/SESSION_HANDOFF.md b/SESSION_HANDOFF.md index 6b19795..6998a98 100644 --- a/SESSION_HANDOFF.md +++ b/SESSION_HANDOFF.md @@ -168,18 +168,15 @@ Three bugs fixed: ## Open punch list -### 1. Physical-device smoke test — PARTIALLY DONE (2026-07-06, Galaxy Fold 7) +### 1. Physical-device smoke test — DONE (2026-07-06, Galaxy Fold 7) -v0.41.1 was installed and launched on a physical Fold 7 via adb. Verified: +v0.41.1 installed via adb and the full device checklist passed on hardware: fold/unfold layout on both screens (incl. the #116 resume path and the -pile-marker fix), safe-area inset resolution, and app launch/restore basics. - -**Still unexercised** from the device checklist: Draw-Three waste fan tap -accuracy (the #106 fix — switch to Draw-Three, drag the visible top waste -card ~10×, confirm it plays *that* card), modal centring on both screens, -drag-and-drop across all pile types, kill-and-restore, and the sync token -flow. AVD is not a substitute — `adb shell input tap` doesn't deliver real -touch events. +pile-marker fix), safe-area inset resolution, Draw-Three waste fan tap +accuracy (#106), modal centring on both screens, drag-and-drop across all +pile types, text rendering, kill-and-restore, and the sync token flow. +Reminder for future gates: AVD is not a substitute — `adb shell input tap` +doesn't deliver real touch events. ### 2. Matomo analytics live validation (independent — NOT a release blocker)