Files
Ferrous-Solitaire/CLAUDE.md
T
funman300 2cfbc32715 docs: add UI-first design principle
Every player-triggered action (new game, undo, draw, pause, open any
overlay, switch mode, etc.) must be reachable from a visible UI
control. Keyboard shortcuts are optional accelerators only — never
the sole entry point. New gameplay features ship with the UI control
alongside the system that backs it.

- ARCHITECTURE.md §1 (Design Principles): add UI-first bullet.
- ARCHITECTURE.md §5 plugin table: rename "Key" column to
  "Shortcut" and add a note that the column lists optional
  accelerators, not primary entry points.
- CLAUDE.md (Bevy Conventions): add a matching hard rule.

Surfaced during smoke testing: the N+N "press again to confirm"
toast collides with the ConfirmNewGameScreen modal because the
keyboard flow is the only entry point. Adding a visible New Game
button (next commit) makes the modal the single source of truth for
the confirm flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:59:38 +00:00

5.4 KiB

Solitaire Quest — Claude Code Instructions

See @ARCHITECTURE.md for full project design, crate responsibilities, data models, and API reference.


Project Layout

solitaire_core/    # Pure Rust game logic — NO Bevy, NO network, NO I/O
solitaire_sync/    # Shared API types — NO Bevy, serde/uuid/chrono only
solitaire_data/    # Persistence + SyncProvider trait + server client
solitaire_engine/  # Bevy ECS systems, components, plugins
solitaire_server/  # Axum sync server binary
solitaire_app/     # Thin binary entry point
assets/            # Source assets — embedded at compile time via include_bytes!()

Build & Test Commands

# Dev run (fast compile via dynamic linking)
cargo run -p solitaire_app --features bevy/dynamic_linking

# Release build
cargo build --workspace --release

# All tests — MUST pass before any commit
cargo test --workspace

# Lint — MUST pass clean (zero warnings)
cargo clippy --workspace -- -D warnings

# Run sync server locally
cargo run -p solitaire_server

# Check a single crate
cargo test -p solitaire_core
cargo clippy -p solitaire_core -- -D warnings

Hard Rules

  • solitaire_core and solitaire_sync must never gain Bevy or network dependencies.
  • No unwrap() or panic!() in game logic. All state transitions return Result<_, MoveError>.
  • Audio assets are embedded at compile time using include_bytes!() in audio_plugin.rs.
  • Card faces (52 PNGs in assets/cards/faces/), card backs (assets/cards/backs/back_N.png), board backgrounds (assets/backgrounds/bg_N.png), and the UI font (assets/fonts/main.ttf) are loaded at runtime via AssetServer::load() and stored as Handle<Image>/Handle<Font> in the CardImageSet, BackgroundImageSet, and FontResource resources. The assets/ directory must ship alongside the binary.
  • Asset-loading systems take Option<Res<AssetServer>> so they degrade cleanly under MinimalPlugins (tests). When CardImageSet is absent, card_plugin falls back to a Text2d rank+suit overlay; when BackgroundImageSet is absent, the board falls back to a solid colour.
  • Atomic file writes only: write to filename.json.tmp, then rename().
  • Passwords and tokens are stored in the OS keychain via the keyring crate — never in plaintext files or logs.
  • Sync runs on AsyncComputeTaskPool — never block the Bevy main thread.
  • All sync backends implement the SyncProvider trait. The SyncPlugin is backend-agnostic — never match on SyncBackend inside a Bevy system.
  • cargo clippy --workspace -- -D warnings must pass clean after every change.
  • cargo test --workspace must pass after every change.

Code Style

  • Use thiserror for error types. Never Box<dyn Error> in library crates.
  • Prefer Into<T> over concrete types in public API function parameters.
  • All public items must have doc comments (///). Private items: comment only when non-obvious.
  • Derive order convention: #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  • Bevy systems: one responsibility per system. Use Events for cross-system communication, never shared mutable state.
  • SQL queries: use sqlx::query! macros (compile-time checked), not raw string queries.
  • No clone() calls in hot paths (game loop systems). Profile before optimising elsewhere.

Bevy Conventions

  • One Plugin per major feature: CardPlugin, AudioPlugin, AchievementPlugin, UIPlugin, SyncPlugin.
  • Resources own shared state. Events communicate between systems. Components own per-entity data.
  • All UI screens are built with Bevy UI (bevy::ui). Never mix UI layout and game logic in the same system.
  • Layout is recomputed on WindowResized — never assume a fixed window size.
  • UI-first. Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.

Git Workflow

  • Commit after each passing phase, not after every file change.
  • Commit message format: type(scope): description
    • feat(core): add draw-three mode validation
    • fix(engine): card z-order during drag
    • test(core): undo stack boundary conditions
    • chore(server): add sqlx migration 002
  • Never commit with failing tests or clippy warnings.
  • Never commit secrets, .env files, or *.db files.

Ask Before Doing

  • Adding a new crate dependency (discuss alternatives first).
  • Changing a type in solitaire_sync (breaking change on both client and server).
  • Altering the database schema (requires a new sqlx migration).
  • Introducing unsafe code anywhere.
  • Changing the merge strategy in solitaire_sync::merge().

Lessons Learned

Add entries here when Claude makes a mistake so it isn't repeated.

  • Bevy's Time resource uses f32 seconds; convert to u64 only when writing to StatsSnapshot.
  • sqlx::migrate!() macro path is relative to the crate root, not the workspace root.
  • keyring on Linux requires a running secret service (e.g. GNOME Keyring or KWallet) — handle Error::NoStorageAccess gracefully and fall back to prompting the user.
  • dirs::data_dir() returns None on some minimal Linux environments — always handle the None case explicitly, do not unwrap.