card_plugin: AndroidCornerLabel used CARD_FACE_COLOUR (dark ~#1a1a1a) as the background and BLACK_SUIT_COLOUR (near-white) for clubs/spades text — both designed for the Terminal theme. On classic PNG cards (white face), this produced an ugly dark box with invisible black-suit text. Switch the corner-label background to Color::WHITE and black-suit text to CARD_FACE_COLOUR (dark ink on white), matching traditional card printing. layout: HUD_BAND_HEIGHT on Android raised 80 → 112 px. The HUD column has 4 flex tiers plus 3 inter-tier gaps (4 px each) and a SPACE_2 = 8 px top offset. With empty tiers still occupying gap height in Bevy's flex layout, the actual rendered HUD could reach ~80 px, overlapping the top card row by up to one text line. 112 px provides ~28 px clearance in the common case (Tiers 1 + 3 visible) and remains workable even when Tier 1 wraps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
CLAUDE.md
version: unified-4.0
0. Role of This File
This document defines:
- Execution rules (what Claude must do)
- System constraints (what Claude must never violate)
- Operational architecture (how code is structured)
For full system design details:
→ ARCHITECTURE.md (authoritative source of truth)
This file overrides all conversational assumptions.
1. System Architecture (Authoritative Mapping)
1.1 Crates
solitaire_core/ # PURE logic (no IO, no Bevy, deterministic)
solitaire_sync/ # Shared API + merge logic
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
assets/ # Runtime assets (except audio + default theme)
1.2 Architecture Source of Truth
- Full system design:
ARCHITECTURE.md - This file NEVER redefines system design
- This file ONLY enforces behavior
2. Hard Global Constraints (NON-NEGOTIABLE)
These override all other instructions.
2.1 Core Determinism
-
solitaire_coreMUST:- be deterministic
- be side-effect free
- never depend on Bevy / IO / async
2.2 Sync Isolation
-
solitaire_sync:- no Bevy
- no IO
- no engine dependencies
-
merge logic must be pure functions only
2.3 Error Policy
- NO
unwrap() - NO
panic!()in runtime/game logic - Core game state mutations MUST return:
Result<T, MoveError>
- Engine / UI state changes follow ECS patterns (Resources, Events) —
they do not return
MoveError - Use
thiserror-derived types for any new error enums outsidesolitaire_core
2.4 Threading Rules
- Sync must run on
AsyncComputeTaskPool - NEVER block Bevy main thread
2.5 Persistence Rules
-
atomic writes only:
- write
.tmp - rename atomically
- write
-
no partial state writes allowed
2.6 Security Rules
-
credentials ONLY via
keyring -
NEVER store secrets in:
- files
- logs
- source code
2.7 Sync System Rules
- All sync backends implement:
trait SyncProvider
SyncPluginMUST be backend-agnostic- NEVER match on backend inside ECS systems
3. Engine Rules (Bevy Layer)
3.1 ECS Design
- systems = single responsibility
- cross-system communication = Events (fire-and-forget triggers)
- persistent shared state = Resources (polled every frame or on change)
- per-entity state = Components only
Events and Resources are both valid communication paths — use Events when
the receiver needs to react once; use Resources when the receiver polls
or when multiple systems read the same value (e.g. SafeAreaInsets,
HudVisibility, LayoutResource).
3.2 Game State Authority
- ONLY
GameStateResourcecan mutate game state - UI systems MUST NOT directly modify core logic
3.3 UI-First Constraint (CRITICAL)
Every player action MUST:
- have a visible UI control
- NOT rely solely on keyboard shortcuts
Keyboard shortcuts are: → optional accelerators only
Exception — UI chrome gestures: Tap-to-toggle visibility of UI chrome (e.g. auto-hiding HUD band) is permitted without a visible button. The gesture MUST:
- affect only chrome visibility, never game state
- restore chrome automatically when any modal opens
- be purely additive (game remains fully playable with chrome always visible)
3.4 Layout System
- recompute on
WindowResized - recompute on
SafeAreaInsetschanged - recompute on
HudVisibilitychanged compute_layoutMUST accepthud_visible: bool; passHUD_BAND_HEIGHTwhentrue,0.0whenfalse- no fixed resolution assumptions
4. Asset System Rules
4.1 Runtime Assets (AssetServer)
Loaded via:
CardImageSetBackgroundImageSetFontResource
Includes:
- cards
- backgrounds
- fonts
4.2 Embedded Assets
Embed via include_bytes!() only when ALL of the following are true:
- the asset is small (< 500 KB uncompressed)
- it changes rarely (not user-customisable)
- a missing file would be a hard crash, not a graceful degradation
Currently embedded:
- Audio — all
.wavfiles inaudio_plugin.rs - Default card theme — shipped via
embedded://scheme inThemePlugin
Do NOT embed card face PNGs, background images, or user fonts —
these are loaded via AssetServer so art can be swapped without recompile.
4.3 Test Compatibility Rule
All asset loaders MUST accept:
Option<Res<AssetServer>>
Must degrade gracefully under MinimalPlugins.
5. Code Standards
5.1 Error Handling
- use
thiserror - no
Box<dyn Error>in libraries
5.2 Public API Rules
- prefer
Into<T>over concrete types - publicly exported functions, traits, and non-trivial types require doc comments
- simple marker components, newtype wrappers, and internal
pubitems used only within the same crate are exempt from doc comment requirements
5.3 Derive Order
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5.4 Performance Rules
- NO
clone()in hot paths - profile before optimizing
5.5 SQL Rules
- ONLY
sqlx::query! - NO raw SQL strings
6. Build & Verification Rules
These are mandatory before ANY commit.
cargo test --workspace
cargo clippy --workspace -- -D warnings
7. Git Workflow Rules
Commit format
type(scope): description
Examples:
- feat(core): add draw-three rules
- fix(engine): correct drag z-order
- test(core): undo boundary cases
Commit conditions
- tests must pass
- clippy must be clean
NEVER commit otherwise
8. Change Control (ASK BEFORE DOING)
Claude must request confirmation before:
- adding dependencies to
solitaire_coreorsolitaire_sync(engine/server crates may add deps without confirmation) - modifying
solitaire_synctypes or theSyncProvidertrait - changing DB schema (migrations are append-only)
- introducing
unsafe - changing the merge strategy in
solitaire_sync::merge - changing the
SyncPayloadwire format (breaking change for existing servers)
9. System Mental Model (IMPORTANT)
Core (rules + deterministic logic)
↓
Engine (Bevy orchestration)
↓
Data layer (persistence + sync)
↓
Server (optional external system)
Core is always the source of truth.
10. Known Platform Pitfalls
Must always be handled explicitly:
All platforms
- Bevy
Timeusesf32 sqlx::migrate!()path is crate-relativedirs::data_dir()may returnNone- Linux may lack keyring backend — handle
keyring::Errorgracefully
Android (active target — not stretch)
- Safe-area insets arrive in frames 1–3 via JNI polling, not at startup; UI that depends on them must handle the zero-inset initial state
- Physical pixels ≠ logical pixels:
SafeAreaInsetsvalues are physical (fromWindowInsetsAPI); divide bywindow.scale_factor()before passing to BevyVal::Px adb shell input tapuses physical pixel coordinates- FiraMono (bundled font) covers: ASCII, card suits U+2660–2666, Arrows U+2190–21FF. It does NOT cover Geometric Shapes (U+25xx) — those render as missing-glyph rectangles on Android
- The gesture/navigation bar at the bottom (≈132px physical on common
devices) is inside the Bevy viewport; use
SafeAreaInsets.bottomto avoid placing interactive elements in that zone HUD_BAND_HEIGHTis 112px on Android vs 64px on desktop; layout constants are#[cfg(target_os = "android")]gated- JNI calls must use
attach_current_thread_permanently— notattach_current_thread— to avoid detach-on-drop panics
11. Forbidden Patterns
- game logic inside Bevy systems
- duplication across crates
- blocking async calls in ECS
- insecure credential storage
- bypassing core logic layer
- hardcoded pixel coordinates in layout — always derive from
compute_layout - Unicode Geometric Shapes block (U+25xx) in UI text — not in FiraMono
- spawning a second
ModalScrimwhile one already exists without first dismissing the existing one (usescrims.is_empty()guard) - reading
SafeAreaInsetsphysical values directly intoVal::Pxwithout dividing bywindow.scale_factor()
12. Execution Rules for Claude
When generating code:
- respect crate boundaries
- minimize diff size
- do not expand scope
- follow existing patterns
- preserve invariants
If unclear: → ask before acting
13. Relationship to ARCHITECTURE.md
| File | Role |
|---|---|
| CLAUDE.md | execution + constraints |
| ARCHITECTURE.md | system design truth |
| Both combined | full system understanding |
14. Modal System Conventions
All full-screen overlay panels MUST use the spawn_modal / ModalScrim pattern
from solitaire_engine::ui_modal.
14.1 Spawn pattern
let scrim = spawn_modal(commands, MyScreenMarker, Z_MODAL_PANEL, |card| {
spawn_modal_header(card, "Title", font_res);
// ... body nodes ...
spawn_modal_actions(card, |actions| {
spawn_modal_button(actions, MyCloseButton, "Done", None,
ButtonVariant::Primary, font_res);
});
});
// Optional: allow clicking the scrim outside the card to dismiss
commands.entity(scrim).insert(ScrimDismissible);
14.2 Guard rule
Before spawning a new modal, check scrims: Query<(), With<ModalScrim>>
and return early if !scrims.is_empty() — unless the new modal is
explicitly replacing the current one (despawn first, then spawn).
14.3 Safe area
Every ModalScrim automatically receives padding.bottom equal to the
logical gesture-bar height via apply_safe_area_to_modal_scrims in
SafeAreaInsetsPlugin. Do not manually add bottom padding to scrim nodes.
14.4 Z-ordering
Use Z_MODAL_PANEL from ui_theme for all modal scrims. Do not use
raw z_index values — they drift and cause ordering bugs.
15. Android Build & Verification
15.1 Build command
cargo apk build --package solitaire_app --lib
adb install -r target/debug/apk/ferrous-solitaire.apk
15.2 Coordinate system reminder
Device physical: 1080×2400. Bevy logical: 900×2000. Scale factor: 1.20.
adb shell input tap X Y takes PHYSICAL coordinates.
To convert from what you see on screen (logical): multiply by 1.20.
15.3 Android-specific test checklist
Before shipping any Android build:
- Safe area insets arrive and shift HUD correctly (check after 3s)
- All modal Done buttons are above the gesture bar
- No Geometric Shapes glyphs in UI text
- HUD band does not overlap the top status bar
- Touch drag-and-drop works on all pile types
16. Context Injection System (AUTOMATIC SCOPE FILTER)
16.1 Purpose
Before generating any response, Claude MUST construct a minimal relevant context set.
This prevents:
- architectural drift
- irrelevant spec loading
- over-engineering
- cross-crate confusion
16.2 Input Classification Step (MANDATORY)
Every request MUST be classified into exactly one task type:
feature
bugfix
refactor
system_design
bevy_system
core_logic
sync
optimization
test
debug
If uncertain → ask clarification.
16.3 Context Selection Engine
After classification, Claude MUST include ONLY the relevant sections below.
16.4 Context Map (CORE RULESET)
feature
Include:
- §2 Hard Global Constraints
- §3 Engine Rules
- ARCHITECTURE.md (crate of target feature only)
- relevant data models (GameState, SyncPayload if needed)
bugfix
Include:
- §2 Hard Global Constraints
- §5 Code Standards
- affected crate boundaries
- relevant system (engine/core/sync only)
refactor
Include:
- §3 Engine Rules
- §5 Code Standards
- §11 Forbidden Patterns
- target crate boundaries
system_design
Include:
- ARCHITECTURE.md (FULL)
- §9 Mental Model
- §1 System Architecture Mapping
core_logic
Include:
- solitaire_core rules only
- GameState model
- MoveError model
- §2.1–2.3 constraints
bevy_system
Include:
- §3 Engine Rules
- ECS rules (Events/Resources/Components)
- UI-first constraint
- relevant plugin system only
sync
Include:
- SyncProvider trait
- merge strategy rules
- solitaire_sync models
- §2.6 Sync Rules
optimization
Include:
- target crate only
- §5.4 Performance Rules
- hot path constraints
test
Include:
- §6 Build Rules
- relevant module
- expected invariants
debug
Include:
- target file/module only
- §2.3 Error Policy
- runtime assumptions relevant to failure
16.5 Context Compression Rules
Claude MUST obey:
- never include full ARCHITECTURE.md unless system_design
- max 2 crates per response unless explicitly required
- prefer function-level context over file-level context
- exclude unrelated plugins/systems
16.6 Context Priority Order
When space is limited:
- Hard Constraints (§2)
- Target crate rules
- Data models
- Only then: architecture snippets
16.7 “No Context Pollution” Rule
Claude must NOT include:
- unrelated crates
- unrelated plugins
- unused data models
- full architecture dumps
- speculative systems
16.8 Self-Check Before Execution
Before writing code, Claude MUST verify:
- Is only relevant context included?
- Is at least one hard constraint present?
- Am I touching more than one crate unnecessarily?
- Am I duplicating ARCHITECTURE.md content?
If any fail → revise context selection.
16.9 Injection Output Format (Internal Model)
Claude should behave as if it constructed:
[SELECTED TASK TYPE]
[MINIMAL REQUIRED RULES]
[MINIMAL ARCHITECTURE SLICES]
[RELEVANT MODELS]
[REQUEST]
16.10 Relationship to ARCHITECTURE.md
- ARCHITECTURE.md = source of truth
- CLAUDE.md = execution constraints
- THIS SECTION = filtering layer between them