Files
funman300 2186f55913
Android Release / build-apk (push) Successful in 4m0s
fix(engine): fix classic-card corner label colours and HUD-band overlap
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>
2026-05-19 14:34:04 -07:00

694 lines
14 KiB
Markdown
Raw Permalink 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.
# 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
```text id="crate_map"
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_core` MUST:
* 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:
```rust id="err_model"
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 outside `solitaire_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
* 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:
```rust id="sync_trait"
trait SyncProvider
```
* `SyncPlugin` MUST 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 `GameStateResource` can 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 `SafeAreaInsets` changed
* recompute on `HudVisibility` changed
* `compute_layout` MUST accept `hud_visible: bool`; pass `HUD_BAND_HEIGHT`
when `true`, `0.0` when `false`
* no fixed resolution assumptions
---
# 4. Asset System Rules
## 4.1 Runtime Assets (AssetServer)
Loaded via:
* `CardImageSet`
* `BackgroundImageSet`
* `FontResource`
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 `.wav` files in `audio_plugin.rs`
* **Default card theme** — shipped via `embedded://` scheme in `ThemePlugin`
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:
```rust id="asset_fallback"
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 `pub` items
used only within the same crate are exempt from doc comment requirements
---
## 5.3 Derive Order
```rust id="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.
```bash id="build_rules"
cargo test --workspace
cargo clippy --workspace -- -D warnings
```
---
# 7. Git Workflow Rules
## Commit format
```text id="commit_fmt"
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_core` or `solitaire_sync`
(engine/server crates may add deps without confirmation)
* modifying `solitaire_sync` types or the `SyncProvider` trait
* changing DB schema (migrations are append-only)
* introducing `unsafe`
* changing the merge strategy in `solitaire_sync::merge`
* changing the `SyncPayload` wire format (breaking change for existing servers)
---
# 9. System Mental Model (IMPORTANT)
```text id="mental_model"
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 `Time` uses `f32`
* `sqlx::migrate!()` path is crate-relative
* `dirs::data_dir()` may return `None`
* Linux may lack keyring backend — handle `keyring::Error` gracefully
**Android (active target — not stretch)**
* Safe-area insets arrive in frames 13 via JNI polling, not at startup;
UI that depends on them must handle the zero-inset initial state
* Physical pixels ≠ logical pixels: `SafeAreaInsets` values are physical
(from `WindowInsets` API); divide by `window.scale_factor()` before
passing to Bevy `Val::Px`
* `adb shell input tap` uses physical pixel coordinates
* FiraMono (bundled font) covers: ASCII, card suits U+26602666,
Arrows U+219021FF. 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.bottom` to
avoid placing interactive elements in that zone
* `HUD_BAND_HEIGHT` is 112px on Android vs 64px on desktop;
layout constants are `#[cfg(target_os = "android")]` gated
* JNI calls must use `attach_current_thread_permanently` — not
`attach_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 `ModalScrim` while one already exists without first
dismissing the existing one (use `scrims.is_empty()` guard)
* reading `SafeAreaInsets` physical values directly into `Val::Px` without
dividing by `window.scale_factor()`
---
# 12. Execution Rules for Claude
When generating code:
1. respect crate boundaries
2. minimize diff size
3. do not expand scope
4. follow existing patterns
5. 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
```rust
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
```bash
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:
```text id="task_types"
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.12.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:
1. Hard Constraints (§2)
2. Target crate rules
3. Data models
4. 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:
```text id="ctx_format"
[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
---
# END CONTEXT INJECTION SYSTEM