684f07746d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.5 KiB
4.5 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_gpgs/ # Google Play Games bridge — STUB ONLY until Android phase
solitaire_app/ # Thin binary entry point
assets/ # Loaded at runtime via Bevy AssetServer only
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_coreandsolitaire_syncmust never gain Bevy or network dependencies.- No
unwrap()orpanic!()in game logic. All state transitions returnResult<_, MoveError>. - No hardcoded bytes in source. All assets go through Bevy's
AssetServer. - Atomic file writes only: write to
filename.json.tmp, thenrename(). - Passwords and tokens are stored in the OS keychain via the
keyringcrate — never in plaintext files or logs. - Sync runs on
AsyncComputeTaskPool— never block the Bevy main thread. - All sync backends implement the
SyncProvidertrait. TheSyncPluginis backend-agnostic — nevermatchonSyncBackendinside a Bevy system. solitaire_gpgsis a stub until Android work begins. Do not write JNI bindings yet; keep the compile-time stub so the trait contract is enforced from day one.cargo clippy --workspace -- -D warningsmust pass clean after every change.cargo test --workspacemust pass after every change.
Code Style
- Use
thiserrorfor error types. NeverBox<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
Eventsfor 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
Pluginper major feature:CardPlugin,AudioPlugin,AchievementPlugin,UIPlugin,SyncPlugin. - Resources own shared state. Events communicate between systems. Components own per-entity data.
- All egui screens live in
solitaire_engine::ui. Never mix egui and Bevy spawn logic in the same system. - Layout is recomputed on
WindowResized— never assume a fixed window size.
Git Workflow
- Commit after each passing phase, not after every file change.
- Commit message format:
type(scope): descriptionfeat(core): add draw-three mode validationfix(engine): card z-order during dragtest(core): undo stack boundary conditionschore(server): add sqlx migration 002
- Never commit with failing tests or clippy warnings.
- Never commit secrets,
.envfiles, or*.dbfiles.
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
unsafecode 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
Timeresource usesf32seconds; convert tou64only when writing toStatsSnapshot. sqlx::migrate!()macro path is relative to the crate root, not the workspace root.keyringon Linux requires a running secret service (e.g. GNOME Keyring or KWallet) — handleError::NoStorageAccessgracefully and fall back to prompting the user.dirs::data_dir()returnsNoneon some minimal Linux environments — always handle theNonecase explicitly, do not unwrap.