Commit Graph

60 Commits

Author SHA1 Message Date
funman300 df4887fb36 docs: update android smoke test runbook 2026-06-08 19:11:02 -07:00
funman300 159774f811 docs: add analytics validation runbook
Build and Deploy / build-and-push (push) Successful in 1m6s
2026-06-08 19:09:22 -07:00
funman300 7fe6ac6c1c docs: catch up handoff and changelog
Build and Deploy / build-and-push (push) Successful in 5m23s
2026-06-08 19:03:40 -07:00
funman300 6193d31497 fix(engine): centre modal cards within usable area (status-bar + gesture-bar)
Build and Deploy / build-and-push (push) Failing after 52s
Web E2E / web-e2e (push) Failing after 4m33s
apply_safe_area_to_modal_scrims now sets both padding.top (status-bar
height) and padding.bottom (gesture-bar height) on every ModalScrim.
With align_items/justify_content: Center on the scrim, the modal card
lands at the visual midpoint of the visible area between the two system
bars, fixing the slight upward shift that occurred when only the bottom
inset was applied.

Also: mark all rewrite-plan phases (0–3) complete; drop obsolete stash
whose 20 files are already incorporated into master; update CLAUDE.md
§14.3 to document both edges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:34:28 -07:00
funman300 26f1b00186 docs(core,data): complete Phases 0–2 of in-place card_game rewrite
Phase 0 – doc fixes (docs/card-game-integration.md):
- Correct stale "no serde" claim: upstream has serde at rev 99b49e62
- Correct take_from_foundation default description (Allowed, not Disallowed)
- Document schema v3→v4 migration and AnyInstruction strategy

Phase 1 – delegate check_win / check_auto_complete to upstream:
- Proptests verify semantic agreement with is_win() / is_win_trivial()
  across 256 random states before delegation

Phase 2 – schema v4 with v3 auto-migration:
- SavedInstruction mirror types kept as legacy compat module (needed by
  solitaire_data::ReplayMove and solitaire_wasm replay layer)
- klondike_adapter.rs: add comprehensive legacy-purpose doc comment
- proptest_tests.rs: add check_auto_complete/check_win semantic proofs
- storage.rs: rename round-trip test to v4, add v3-migrates-to-v4 test

Also track the rewrite plan (docs/in-place-card-game-rewrite-plan.md).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:59:18 -07:00
funman300 d45b7cb82b feat(e2e): add Playwright browser test suite for web routes
Build and Deploy / build-and-push (push) Successful in 1m6s
Web E2E / web-e2e (push) Successful in 4m40s
solitaire_server/e2e/:
- smoke.spec.js: verifies /play-classic loads, exposes window.__FERROUS_DEBUG__
  bridge, keyboard parity (Space=draw, U=undo), debug failure report, and
  replay payload builder exports schema-v2 moves.
- gameplay_review.spec.js: HUD/controls render check, stock-click + undo
  player flow, draw-mode toggle, autonomous play invariant batch, and
  cycle-detection regression guard.
- cycle_metrics.js: headless cycle-rate analysis tool; run via
  `npm run review:cycles` with configurable policy, game count, and
  thresholds. Regression gate baked into package.json scripts.
- playwright.config.js: targets the local server at http://localhost:8080.
- package.json / package-lock.json: @playwright/test 1.60.0.

.gitea/workflows/web-e2e.yml:
- Runs on pushes to solitaire_server/, solitaire_wasm/, solitaire_core/,
  or Cargo changes. Starts the server binary, waits for /health, runs
  the full Playwright suite, uploads test-results/ on failure.

docs/testing-architecture.md: documents the three-tier test strategy
  (unit → Playwright smoke → cycle regression) and the __FERROUS_DEBUG__
  bridge contract.

scripts/update_quaternions_deps.sh: helper to bump the Quaternions
  registry deps (klondike, card_game) by version and run the full
  safety gate including deterministic replay checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:40:30 -07:00
funman300 1cdb78caf2 chore: cargo fmt across workspace; add analytics domain to CSP
Build and Deploy / build-and-push (push) Successful in 4m46s
- Apply cargo fmt to solitaire_engine, solitaire_server formatting.
- solitaire_server/src/lib.rs: add https://analytics.aleshym.co to
  script-src, img-src, and connect-src so the analytics beacon loads
  without a CSP violation.
- docs and README updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:21:32 -07:00
funman300 389fdd1fb0 docs: add card-game integration guide (closes #76)
Full gap analysis between Quaternions/card_game and solitaire_core,
integration steps 1-7 (all now complete), and references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 18:04:41 -07:00
funman300 0a6eb8c610 Revert "docs: update integration doc to reflect klondike v0.2.0 / card_game v0.3.0"
This reverts commit bb92bb333b.
2026-05-29 14:06:00 -07:00
funman300 bb92bb333b docs: update integration doc to reflect klondike v0.2.0 / card_game v0.3.0
Both upstream issues are now merged:
- PR #13 (closes #10): ScoringConfig with 5 configurable deltas lands
  in KlondikeConfig; KlondikeStats gains flip_up_bonus_count and
  move_from_foundation_count; score() takes &ScoringConfig
- PR #12 (closes #11): MoveFromFoundationConfig (Allowed/Disallowed)
  lands in KlondikeConfig; is_instruction_valid enforces it

Doc changes:
- "Already has" table updated with ScoringConfig, MoveFromFoundationConfig,
  richer KlondikeStats counters, and version numbers (v0.3.0 / v0.2.0)
- Gap 1 scoring table gains a "Handled by" column showing which deltas
  upstream now owns vs. which remain in our adapter (undo penalty,
  recycle-with-free-allowance, score floor, time bonus)
- Gap 1 adds note that ScoringConfig::recycle is a flat delta and cannot
  express the "N free recycles then penalty" WXP rule
- Gap 4 marked as upstream merged; notes that upstream default is
  MoveFromFoundationConfig::Allowed — we must explicitly set Disallowed
- Integration path: steps renumbered (8→7), step 3 now configures
  MoveFromFoundationConfig, step 4 splits upstream-handled vs.
  adapter-owned scoring; dependency versions pinned
- References updated with PR links and release commit hashes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:05:35 -07:00
funman300 d761a150d7 chore: rename app from Solitaire Quest to Ferrous Solitaire
Build and Deploy / build-and-push (push) Successful in 4m40s
Updates all in-tree references:
- Android package: com.solitairequest.app → com.ferrousapp.solitaire
- APK name: solitaire-quest → ferrous-solitaire
- Data dir: solitaire_quest → ferrous_solitaire (across all 6 data modules + engine)
- Keyring service: solitaire_quest_server → ferrous_solitaire_server
- Android Keystore key: solitaire_quest_token_key → ferrous_solitaire_token_key
- Gitea repo: Rusty_Solitare → Ferrous-Solitaire (also fixes "Solitare" typo)
- Renamed pkg/solitaire-quest* → pkg/ferrous-solitaire*
- Updated ArgoCD, docker-compose, CI workflow, build script, all docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:23:49 -07:00
funman300 51fecb24b0 chore: remove stale docs, mockups, and one-off artifacts
Build and Deploy / build-and-push (push) Failing after 43s
Delete CLAUDE_PROMPT_PACK/SPEC/WORKFLOW (superseded by CLAUDE.md),
SESSION_HANDOFF files, old android investigation notes, phase-plan docs
under docs/superpowers/, and all docs/ui-mockups/ (HTML+PNG mockups
from the pre-Terminal design pass). Also removes local artifacts:
analytics_impl_prompt.md, review_project.py, delete_runs.sh, ruvector.db
files, and the stray solitaire_wasm/solitaire_server/ build artefact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:15:11 -07:00
funman300 8325bf6cf7 chore: rename app from Solitaire Quest to Ferrous Solitaire
Replace all display-name occurrences across web pages, Rust source,
docs, and Cargo metadata. Update localStorage token key from sq_token
to fs_token. Tagline "Klondike Solitaire" retained as genre descriptor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:04:45 -07:00
funman300 859b69b3c5 fix(android): A2/A3/A4 — APK build doc, dead refs, modal hit targets
A2: docs/ANDROID.md — remove stale "permanent fix to come" note;
    clarify --lib is the canonical command; root-cause the upstream
    cargo-apk bug. SESSION_HANDOFF.md closes the open item.

A3: Remove dead CARD_PLAN.md references from four source module
    doc comments (theme/importer.rs, theme/plugin.rs, assets/mod.rs,
    assets/svg_loader.rs). Also fix stale "future picker UI" language
    in plugin.rs (picker shipped in Phase 7).

A4: ui_modal.rs spawn_modal_button — add min_height: Val::Px(48.0)
    so every modal action button meets Material's 48 dp touch target
    minimum. Modal button height was 42 px (2×SPACE_3 + TYPE_BODY_LG);
    now clamped to 48 px minimum. Cards at 40 dp on 360 dp phones are
    layout-constrained (7 columns) and cannot be widened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:55:30 -07:00
funman300 918d83420b docs: update all project docs to reflect Phase 8 + Android work
- CLAUDE.md unified-3.1 → unified-4.0: narrowed error policy, relaxed
  ECS/embed/API rules, added Android pitfalls, modal conventions (§14),
  Android build guide (§15), context injection system (§16), auto-hide
  HUD chrome exception in UI-first rule
- ARCHITECTURE.md: Android → Active platform; add Android to sync table;
  add SafeAreaInsets + HudVisibility to Key Resources; add solitaire_wasm
- CLAUDE_SPEC.md: add solitaire_wasm crate; communication: events → events and resources
- CLAUDE_PROMPT_PACK.md: fix §8 typo; narrow dep rule to core/sync only
- SESSION_HANDOFF.md: add §5b Android UX punch list; resume prompt unified-4.0
- docs/android/PLAYABILITY_TODO.md: add P5 section (UX-1/UX-5b/UX-7/BUG-3)
- docs/SESSION_HANDOFF.md: mark as archived (Phase 2 era)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:21:01 -07:00
funman300 8a3e30bd16 fix(android): P3 keyboard-hint sweep + clipboard JNI verified
Suppress all remaining keyboard-accelerator chips/labels on Android:
- spawn_modal_button (ui_modal.rs): single cfg gate covers every modal
  across all 13+ callers (onboarding, pause, confirm, game-over, restore,
  play-by-seed, home, help, profile, stats, leaderboard, settings, achievement)
- home_plugin.rs: mode-card hotkey chips (N/C/Z/X/T) gated off
- replay_overlay.rs: [SPACE]/[ESC]/[←→] footer hint text gated off;
  mode-indicator text kept
- help_plugin.rs: kbd chip containers gated off; description text kept

Clipboard JNI verified on Pixel 7 AVD (Android 14): added temporary
KEYCODE_C test hook, logcat confirmed "clipboard JNI OK", hook reverted.
Both JNI bridges (keystore + clipboard) are now confirmed working on device.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:22:40 -07:00
funman300 2a206b994c fix(android): wrap sync HTTP tasks in per-call Tokio runtime
reqwest/hyper-util's GaiResolver calls tokio::runtime::Handle::current()
which panics with "no reactor running" when driven by Bevy's
AsyncComputeTaskPool (async-executor, not Tokio).  Fixed all three spawn
sites in sync_plugin.rs (start_pull, handle_manual_sync_request,
push_replay_on_win) and the push_on_exit fallback by wrapping each HTTP
future in tokio::runtime::Builder::new_current_thread().enable_all().

Also fixes a clippy type_complexity warning in hud_plugin.rs by
extracting HudScoreFont / HudMovesFont / HudTimeFont type aliases for
the update_hud_typography query parameters.

Closes P4 AVD JNI bridge test: keystore JNI verified working on
Android 14 x86_64 AVD (load_access_token returned NotFound correctly);
clipboard JNI compiled and linked, runtime test deferred to a real-device
session with a won game and active sync server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:55:20 -07:00
funman300 ae7c6c97f1 fix(android): P3 icon density buckets + P4 B0004 investigation
P3 — App-icon density buckets:
- Created solitaire_app/res/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/
  ic_launcher.png from assets/icon/ (48→mdpi, 64→hdpi, 128→xhdpi,
  256→xxhdpi+xxxhdpi). aapt downscales oversized buckets; no quality loss.
- Added resources = "res" to [package.metadata.android] so cargo-apk/aapt
  packages the mipmap tree into the APK.
- Added icon = "@mipmap/ic_launcher" to [package.metadata.android.application]
  so the launcher references the density-bucketed icon instead of the
  default grey system icon.

P3 — Density-aware card scaling: investigated, no code change required.
  WindowResized fires with logical pixels; 256×384 card textures are
  downscaled on all current phone targets (40dp logical → 120px physical
  at 3× DPI). Upscaling only occurs on tablets wider than ~765dp at 3× DPI.

P4 — B0004 hierarchy warnings: investigated, no fix required.
  .despawn() is recursive in Bevy 0.18; warnings are startup timing
  artifacts (UI components propagating before parent initialises), not
  gameplay bugs. No crashes or defects in 2+ min AVD runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:53:38 -07:00
funman300 016fb7214d fix(android): responsive HUD typography + portrait orientation lock
Closes the final two P2 Android playability items:

1. HUD typography — new `update_hud_typography` system fires on
   `WindowResized` and adjusts Tier-1 font sizes: below 480 logical px
   Score drops HEADLINE(26)→BODY_LG(18) and Moves/Timer drop
   BODY_LG(18)→CAPTION(11), so all three fit in the 180dp HUD column
   on a 360dp phone without wrapping.

2. Orientation lock — `[package.metadata.android.application.activity]`
   with `orientation = "portrait"` in solitaire_app/Cargo.toml; cargo-apk
   maps this to `android:screenOrientation="portrait"` in the generated
   AndroidManifest.xml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:44:26 -07:00
funman300 948864e653 feat(android): long-press opens radial menu as right-click alternative
Touch screens have no right mouse button, so right-click radial was
inaccessible on Android. New system radial_open_on_long_press counts
up while a touch is held on a face-up card without crossing the drag
threshold; after 0.5 s it transitions RightClickRadialState to Active,
which the existing visual overlay and destination-ring infrastructure
then renders unchanged.

Three supporting changes to wire up the touch-driven confirm path:

- radial_track_cursor: falls back to the first active Touches position
  when cursor_world returns None, so the hover ring tracks a sliding
  held finger on Android.

- radial_handle_release_or_cancel: confirms on Touches::iter_just_released
  (finger lift) in addition to right-mouse release. Cancels on
  Touches::iter_just_canceled. No new event reader — uses the Touches
  resource which is already in scope after the track_cursor addition.

- handle_double_tap: skips when the radial is active. Guards the
  narrow edge case where the finger lifts on the exact same frame
  as the 0.5 s long-press threshold fires; prevents a spurious
  double-tap move from racing with the radial confirm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:23:24 -07:00
funman300 76a754d8e5 fix(android): improve touch drag responsiveness
Two improvements to drag responsiveness on Android:

1. Guard start_drag against touch-simulated mouse presses.
   start_drag (mouse path) now bails when Touches::iter_just_pressed()
   finds an active touch, so touch_start_drag always owns drag state on
   touch-screen devices. Without the guard, Bevy/Winit versions that
   synthesise MouseButton::Left from the primary touch would have the
   mouse drag path claim drag state first (start_drag runs before
   touch_start_drag in the system chain), leaving the card tracked via
   cursor_world instead of the Touches resource.

2. Lower mobile drag commit threshold 10 px → 8 px.
   Matches Android ViewConfiguration.getScaledTouchSlop() exactly.
   Smaller threshold reduces the snap-to-finger displacement at commit
   and makes drag feel more immediate.

Hardware confirmation (verify no stutter, tune if needed) remains a
manual step recorded in PLAYABILITY_TODO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:16:27 -07:00
funman300 9fb59c7d47 fix(android): lime flash on double-tap auto-move confirmation
When handle_double_tap recognises a double-tap and fires MoveRequestEvent,
the moved card(s) are immediately tinted STATE_SUCCESS (lime #acc267) with
a 0.35 s HintHighlight so the player sees visual confirmation before the
card animation begins.

- Priority 1 (single top card): flashes that card only.
- Priority 2 (whole face-up stack): flashes every card in drag.cards.

Reuses the existing tick_hint_highlight cleanup path (restores sprite
to WHITE when timer expires) so no new system or component is needed.
The flash duration (0.35 s) slightly outlasts a typical card animation
(~0.3 s), giving the tint a brief moment at the destination before clearing.

Marks P1 "Double-tap auto-move visible feedback" as closed in
PLAYABILITY_TODO (hardware trigger-verification still manual).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:10:38 -07:00
funman300 d714a11cfb fix(android): adaptive tableau fan fraction fills portrait viewport
On a 360 dp portrait phone the card width is set by the 9-column
horizontal packing (360/9 = 40 dp); the fixed 0.25 fan fraction then
places the worst-case 13-card column in the top ~44 % of the screen,
leaving the bottom 56 % empty black.

`compute_layout` now solves for the fan fraction that exactly uses the
available vertical space below the tableau row:

    ideal = avail / (12 * card_height)

On height-limited (desktop) windows ideal ≈ 0.25 and the clamp to the
minimum keeps existing behaviour. On width-limited (portrait phone)
windows the fan expands — ≈ 0.84 at 360 × 800 dp — stretching the
tableau to fill the screen.

Both `tableau_fan_frac` and `tableau_facedown_fan_frac` (scaled
proportionally) are stored on the `Layout` struct. `card_plugin` and
`input_plugin` read from the struct so rendering and hit-testing stay
in sync at every viewport size.

Three new regression tests:
- portrait phone expands fan_frac beyond desktop minimum
- expanded fan fits inside phone viewport (no overflow)
- desktop fan_frac stays at minimum 0.25

Closes P1 "Portrait-first card spacing" in PLAYABILITY_TODO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:05:17 -07:00
funman300 e107f5e218 fix(android): 48dp min hit targets on action buttons
Release / Build · Linux x86_64 (push) Has been cancelled
Release / Build · Android APK (push) Has been cancelled
Release / Publish GitHub Release (push) Has been cancelled
Action buttons sized to text + 8 px padding made "Undo" end up
~46 x 33 px — fine for a mouse but at the threshold of a finger.
Adds `min_width: 48 px` and `min_height: 48 px` to the button
Node so every button meets Material's 48 dp thumb-target guideline.

Applied universally; the floor is a no-op for buttons whose
content already exceeds 48 px on either axis (Menu, Modes,
New Game, Pause, Help all clear 48 px wide; height was the
binding constraint at ~33 px).

Closes P1 #2 of docs/android/PLAYABILITY_TODO.md. Engine tests
pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:53:40 -07:00
funman300 463b7465ed fix(android): hide keyboard-hint chips on action buttons
The U / Esc / F1 / N caption chips next to the HUD action buttons
are meaningless on a touch device and visibly clutter the
narrow-viewport action row (visible as "Esc A [] N" in the v0.22.3
screenshot). `spawn_action_button` now rebinds `hotkey` to `None`
under `#[cfg(target_os = "android")]` so the chip-spawn branch is
skipped on touch builds.

Menu / Modes chevrons are unaffected — they indicate dropdown
behaviour and still apply on touch. Other hint surfaces
(onboarding, pause modal Esc hint, mode-card chips, replay
footer, modal toggle chips, help screen) live behind navigation
and are tracked as a P3 sweep in PLAYABILITY_TODO.md.

Closes P1 #1 of docs/android/PLAYABILITY_TODO.md. 855 engine
tests pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:52:12 -07:00
funman300 92a5ebb15e fix(android): lower MIN_WINDOW floor so phone viewports lay out correctly
`compute_layout` runs `window.max(MIN_WINDOW)`, which acts as a
component-wise floor: any window smaller than MIN_WINDOW on either
axis gets clamped up. The previous floor of 800x600 was set with
desktop in mind, but on Android the OS-provided window size is the
device resolution (~360 dp wide on a typical phone) and the clamp
silently re-laid the board for an 800 dp width.

Side effect: total grid width (9 * card_width) became ~800 px on a
360 dp viewport, so the leftmost foundation x-position fell past
-180 and the rightmost tableau pile past +180 — both clipped at
the visible edges, matching the v0.22.3 hardware screenshot.

Lowered MIN_WINDOW to 320x400, below the smallest reasonable phone
(~360x640), so every real device flows through compute_layout
unclamped. The floor is preserved as a sentinel against degenerate
windows (Bevy can briefly report 0-size during startup or after
minimisation on some compositors). Desktop's "minimum supported
playable size" is enforced separately via WindowResizeConstraints
in solitaire_app.

Updates `layout_below_minimum_clamps_to_minimum` to use values
below the new floor, and adds a new regression test
`phone_portrait_layout_fits_horizontally` that asserts all 13
piles fit inside a 360 x 800 dp viewport.

Closes P0 #4 of docs/android/PLAYABILITY_TODO.md. 855 engine tests
pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:48:56 -07:00
funman300 89a21c0587 fix(android): wrap HUD column and action button row on narrow viewport
The v0.22.3 hardware screenshot showed the 6-button action row
(~510 px when laid out) overflowing into a 360 dp viewport from
the right anchor, with Menu and Undo clipped off-screen left and
Pause/Help/Modes/New_Game overlapping the left HUD column's
Score / Moves / Timer text.

Cap both clusters at `max_width: 50 %` so on mobile each takes
half the viewport (~180 px) and on desktop the cap is wider than
either cluster's natural width so the existing single-line
layout is preserved.

- Action button row: adds `flex_wrap: Wrap`, `row_gap`, and
  `justify_content: FlexEnd` so the row breaks to multiple
  right-aligned lines instead of clipping. 6 buttons become 2-3
  lines of 2-3 buttons.
- HUD column tier rows: add `flex_wrap: Wrap` and `row_gap` to
  the shared `row_node` helper so a long Mode/Challenge/Draw-cycle
  combo soft-wraps onto two lines instead of pushing into the
  action button column.

Closes P0 #2 of docs/android/PLAYABILITY_TODO.md. All 854 engine
tests pass; clippy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:45:41 -07:00
funman300 304cb050a7 docs(android): close P0 safe-area + card-back items in playability TODO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:41:28 -07:00
funman300 47f02a60ae docs(android): add screenshot-driven playability TODO
Captures the gap between "boots without crashing" (v0.22.3 status)
and "actually playable on a phone." Tracks P0-P4 work items grouped
by impact: safe-area, HUD layout, card-back rendering, viewport
overflow, touch UX, density.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:36:33 -07:00
funman300 dd970215cc fix(engine): drop card-face border to remove gray-corner artifact
Player feedback after the 2-colour revert: "I do not like the
grey corners on the cards." The visible artifact was anti-
aliasing physics — the 1 px suit-coloured stroke (red for
hearts/diamonds, near-white for clubs/spades) faded through
gray pixels into the dark play surface at each rounded corner,
producing a visible "gray sliver" at the four arcs of every
card.

Fix: drop the stroke entirely. The card body fill defines the
shape against the play surface; the 5-unit brightness gap
between `#1a1a1a` body and `#151515` surface is enough to read
as a card edge without an explicit stroke. Anti-aliasing on a
fill-only rounded rect blends `#1a1a1a → #151515` over a few
pixels — barely perceptible compared to the
`stroke → transparent` gradient that produced the artifact.

### Changes

- `card_face_svg.rs`: removed `stroke="{colour}" stroke-width="2"`
  from the card body rect. Reverted the 1 px stroke inset back
  to `(x=0, y=0, width=256, height=384)` since there's no
  longer a stroke to keep inside the pixmap. Module-level
  comment updated to document the reasoning.
- `design-system.md` § Game Cards line 225 updated: "Border:
  1px solid in suit color" → "Border: none." with the
  artifact rationale recorded as audit trail.
- `card_face_svg_pin.rs` rebaselined: all 52 face hashes drift
  (every card's perimeter pixels changed); 5 back hashes
  unchanged.

Workspace clippy + cargo test --workspace clean. 1191 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:41:54 -07:00
funman300 ddb65403c2 feat(engine): revert to traditional 2-colour deck with saturated red + near-white
Per player feedback after the brief 4-colour-deck experiment:
"can we make the card suit colors the same as a regular
solitaire game would." Reverts the 4-colour split (`62b61cc`)
and bumps both 2-colour hues to read more like a real
Microsoft-Solitaire-on-dark-mode deck.

### Constants

- `RED_SUIT_COLOUR`: `#fb9fb1` (Terminal pink, then briefly
  hearts-only) → `#e35353` (saturated red). More chromatic, less
  pastel; reads as "the red suit" rather than "a Terminal-
  themed pink." Visually distinct from `ACCENT_PRIMARY`
  `#a54242` (the brick-red CTA accent) so chrome and suit don't
  collapse to the same hue.
- `BLACK_SUIT_COLOUR`: `#d0d0d0` (matched `TEXT_PRIMARY`) →
  `#e8e8e8` (near-white). Bumped slightly brighter so it reads
  as a chromatic-neutral counterpart to the new saturated red,
  not as "the same gray as body text." `TEXT_PRIMARY_HC`
  (`#f5f5f5`) is still brighter for the high-contrast boost
  path.
- `RED_SUIT_COLOUR_HC`: `#ff8aa0` (pinkish boost matching the
  v0.21.0 pink default) → `#ff6868` (brighter saturated red).
  Now reads as "more chromatic" than the new default red, not
  "less saturated."
- `DIAMOND_SUIT_COLOUR` and `CLUB_SUIT_COLOUR` deleted — the
  4-colour split is gone, hearts/diamonds re-pair under
  `RED_SUIT_COLOUR` and clubs/spades under
  `BLACK_SUIT_COLOUR`.

### `card_face_svg.rs`

- Module-level constants collapse from four (`SUIT_HEART` /
  `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`) back to two
  (`SUIT_RED` / `SUIT_DARK`) at the new saturated-red /
  near-white values.
- `suit_paint()` reverts to the 2-colour pairing: hearts
  filled-red, diamonds outlined-red, spades filled-near-white,
  clubs outlined-near-white. Filled-vs-outlined glyph
  differentiation stays the always-on CBM fallback.

### `card_plugin.rs`

- `text_colour()` reverts to a `card.suit.is_red()`
  bifurcation. Comment block updated to reflect the new
  truth table: red suits → saturated red (or CBM lime / HC
  brighter red); dark suits → near-white (or HC brighter
  near-white).

### Tests

Test block restructured back to the pre-4-colour shape: two
red/black pairing tests instead of one 4-colour distinctness
test. CBM/HC compose tests retuned to the 2-colour world (red
suits compose, dark suits compose; no separate diamonds-immune
or clubs-immune cases). 1191 passing / 0 failing — net 0 from
the prior commit (3 tests removed: the 4-colour distinctness
test + the diamonds/clubs-immune test; 2 tests added back: the
red-pairing + dark-pairing tests; existing tests amended to
new colour assumptions).

### `card_face_svg_pin`

All 52 face hashes drift (every suit's colour shifted); 5 back
hashes unchanged. Surgical rebaseline.

### `design-system.md`

§Suit Colors retitled "Two-color traditional pairing", table
updated with the new hex values, CBM section text simplified
back to red→lime swap on both red suits.

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:35:36 -07:00
funman300 62b61cc786 feat(engine): switch card fronts to 4-colour deck
Hearts pink (`#fb9fb1`), Diamonds gold (`#ddb26f`), Clubs lime
(`#acc267`), Spades gray (`#d0d0d0`) — each suit picks up its
own base16-eighties accent so a player scanning the table can
distinguish the suit by hue alone (faster recognition than the
2-colour traditional red/black scheme; common in poker decks).
All four colours already exist in the palette as semantic
state-token accents, so this is a pure remapping at the suit-
glyph site, not a palette extension.

The outlined-glyph differentiation (♦ ♣ outlined, ♥ ♠ filled)
is preserved on top of the colour split — it stays the always-
on colour-blind fallback per `design-system.md` §Accessibility,
and matters more than ever now that CBM hearts (lime) and
default clubs (lime) share a hue.

### Changes

- `card_face_svg.rs`: split `SUIT_RED` / `SUIT_DARK` into four
  per-suit constants (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB`
  / `SUIT_SPADE`). `suit_paint()` returns each suit's own
  colour. Card border picks up the suit colour automatically
  via the existing `(colour, paint)` destructure.
- `card_plugin.rs`: new `DIAMOND_SUIT_COLOUR` + `CLUB_SUIT_COLOUR`
  constants; `text_colour()` rewritten as a per-suit match (was
  red/black bifurcation). Both rendering paths (PNG production +
  constant fallback under MinimalPlugins) stay in lockstep.
- CBM behaviour clarified: only hearts swap to lime now;
  diamonds + clubs + spades are already hue-distinct from
  the heart pink and stay unchanged. Under CBM the heart
  (lime) and club (lime) share a hue but stay distinguishable
  via the always-on filled-vs-outlined glyph differentiation.
- HC behaviour: only hearts (→ HC red) and spades (→ HC white)
  have defined boosts. Diamonds (gold) and clubs (lime) are
  already mid-luminance accents and stay at their default.
  New test `text_colour_diamonds_and_clubs_are_immune_to_accessibility_flags`
  pins all four flag combinations as no-ops for the gold +
  lime suits.
- `design-system.md` §Suit Colors retitled "Four-color deck"
  with the 4-colour table; CBM section text updated to
  describe the hearts-only swap and the hearts/clubs hue
  collision under CBM.
- `card_face_svg_pin.rs` rebaselined: 26 hashes drift
  (13 clubs + 13 diamonds — the two suits whose colours
  changed). Hearts, spades, and the 5 backs all keep their
  prior hashes. Surgical scope, exactly what the pin test
  was designed to surface.

### Tests

1191 passing / 0 failing — net 0 from the prior baseline:
two old 2-colour tests removed
(`text_colour_is_red_for_hearts_and_diamonds`,
`text_colour_is_black_for_clubs_and_spades`), one consolidated
4-colour test added
(`text_colour_4_colour_deck_assigns_each_suit_its_own_hue`)
plus a pairwise-distinct invariant guard, and one new test
covering the gold/lime suits' immunity to CBM/HC flags. Six
existing CBM/HC tests rewritten to use only the suits each flag
actually affects under the new scheme (hearts for CBM, hearts +
spades for HC).

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:00:55 -07:00
funman300 a292a7ead0 feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan
primary accent everywhere it surfaces — splash boot screen,
home menu glyphs, action chevrons, replay overlay banner +
scrub fill + chip border, achievement checkmarks, leaderboard
#1 indicator, radial menu fill, focus ring, card-back canonical
badge, etc. — with `#a54242` from the same base16-eighties
family as the existing pink suit colour.

Knock-on changes that all land in this commit per the
lockstep rule:

- ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER
  (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85
  alpha). Module-level palette comment + STOCK_BADGE_FG +
  CARD_SHADOW_ALPHA_DRAG doc strings updated to match.
- card_plugin.rs: card_back_colour(0) now returns the brick-red
  ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from
  cyan to lime #acc267 — the CBM alternative needs to stay
  hue-distinct from the new red-family primary, lime is the
  next-best non-red base16-eighties accent. text_colour doc
  + CBM tests renamed cyan→lime in lockstep
  (text_colour_color_blind_mode_swaps_red_suits_to_lime).
- card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical
  Terminal back).
- splash_plugin.rs / ui_modal.rs / replay_overlay.rs /
  selection_plugin.rs: descriptive "cyan" comments swapped to
  "accent" / "primary-accent" wording so the doc strings stay
  decoupled from any specific hue. Future palette tweaks won't
  require comment churn.
- design-system.md: YAML token frontmatter updated (primary,
  surface-tint, suit-red-cb, primary-container,
  on-primary-container, inverse-primary). Palette table gains
  a project-specific `base08` slot for the new red. CTA /
  Selection / Card-back badge / Primary button / Bottom-bar
  active-icon / glow / CBM swap text all retuned. Historical
  references preserved (e.g. "Was cyan #6fc2ef before the
  2026-05-08 swap") so the audit trail stays in the spec.
- card_face_svg_pin.rs: rebaselined. Exactly one hash drift
  (back_0 — the canonical Terminal back's badge changed
  colour). Other 56 hashes identical (face SVGs don't
  reference the accent; back_1..4 use unchanged accents). The
  one-hash-drift signal confirms the change scope was
  surgical.

Workspace clippy + cargo test --workspace clean, 1184 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:30:35 -07:00
funman300 dd101b3d54 fix(engine): render bottom-right card glyph upright (no 180° rotation)
The user noticed the bottom-right large suit glyphs were
rendering upside-down — point-up hearts, stem-up spades — because
the SVG transform pipeline applied a `rotate(180)` to match the
traditional playing-card inverted-corner convention.

That convention exists so a card reads correctly when flipped or
read from the opposite side of the table. Single-orientation
digital play doesn't benefit from it; most modern digital decks
have abandoned it. User preference is upright.

Drops the rotate from face_svg's bottom-right `<g transform>`
and adjusts the translate so the visible glyph still lands at
(178, 286)–(242, 350) — same screen footprint, same scale, just
no flip.

design-system.md § Game Cards updated in lockstep — line 220
no longer says "rotated 180°", instead documents the deliberate
deviation from the traditional convention.

Knock-on lockstep changes in this commit:
- EXPECTED in tests/card_face_svg_pin.rs rebaselined: 52 face
  hashes shift, 5 back hashes unchanged.
- assets/cards/faces/*.png regenerated (52 face PNGs).
- solitaire_engine/assets/themes/default/*_*.svg regenerated
  (52 theme face SVGs that production rasterises at startup).

Workspace clippy + cargo test --workspace clean. Pin test
passes against the new hashes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:09:55 -07:00
funman300 56233687b0 docs(ui): add card-face artwork migration plan
Lays out the lockstep migration from legacy white-card PNGs +
constants to the Terminal aesthetic. Steps 4 + 5 (artwork +
constant + test updates) must land in one commit so the PNG
path and the constant-fallback path don't visually diverge.

Tracks Option D from the SESSION_HANDOFF Resume prompt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:08:04 -07:00
funman300 39b84965b6 docs(ui): add Terminal desktop-adaptation spec
Closes the spec gap flagged after v0.20.0: the 24 mockups in
docs/ui-mockups/ are 23 mobile + 1 desktop, but desktop is still
the primary delivery surface. Stitch's variant generation kept
timing out on layout-only adaptation prompts, so the deterministic
fix is rules-based: a markdown spec that captures (a) the desktop
viewport assumptions, (b) seven universal adaptation rules that
apply to every screen, and (c) per-screen geometry rules for the
priority surfaces (Game Table, Win Summary, Settings, Help, Pause,
Home, Splash, Stats, Profile / Achievements / Theme Picker / Daily
Challenge).

Why rules > visual mockups for this gap:

- Apply uniformly to every screen — including the 9 missing-plugin
  surfaces (splash, challenge, time-attack, weekly-goals, leader-
  board, sync, level-up, replay-overlay, radial-menu) that have
  only mobile mockups today.
- Reference-able from code comments and commit messages without
  loading an image.
- Layout-agnostic by construction: tells the engine "use percent /
  flex / min(720, 50%) widths" instead of pinning a specific
  desktop pixel layout.
- Cheaper than re-running Stitch generation per screen, which is
  flaky for layout-only adaptation work.

Cross-check confirms that v0.20.0's port (modal scaffold, toasts,
table chrome, card chrome, gameplay-feedback, splash cursor) is
already layout-agnostic — the spec gap mattered for *next* ports,
not the work that just shipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 19:08:58 -07:00
funman300 fa7f98ac52 docs(ui): land the Terminal design system + 24-mockup library
Adds the spec the recent visual-identity port pass referenced:

- design-system.md — base16-eighties palette, type scale, spacing
  scale, motion budget, component library, accessibility notes
  (color-blind toggle, high-contrast mode, glyph differentiation),
  and the canonical "Terminal" card-back theme.
- 24 Stitch-rendered mockups (HTML + PNG): 12 redesigned existing
  screens, 1 desktop home variant, 2 onboarding steps, and 9
  missing-plugin screens (splash, challenge, time-attack,
  weekly-goals, leaderboard, sync, level-up, replay, radial-menu).

These mockups are the source the engine plugins were ported
against in commits 0d477ac through 9891ae4 (token system,
modal scaffold, gameplay-feedback layer, toasts, table chrome,
card chrome, splash cursor, hint highlight). Future plugin work
should diff against the matching mockup before touching pixels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:47:57 -07:00
funman300 59424a370c docs(android): developer setup + build runbook
Captures the toolchain install for Debian 13 (the path Quat ran on
this dev box, including the JDK 21 / unzip / SDK-licence prompts),
the `cargo apk build` invocation, the cosmetic post-pass panic
workaround, and the table of what's wired vs. stubbed for the
android target. Runnable on a fresh box from a clone — no
machine-local context required.

Pairs with the workspace cfg-gating in fb8b2ac. Future Phase-Android
work (dirs::data_dir port, JNI ClipboardManager, Android Keystore,
gpgs) is listed as the not-yet-done section so a contributor can
pick it up without re-deriving the punch list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:36:36 +00:00
funman300 063269c70e docs: update repo URL references to corrected Rusty_Solitaire spelling
The GitHub repo was renamed from Rusty_Solitare to Rusty_Solitaire
(adding the missing 'i'). The local origin remote has been updated
via `git remote set-url`; this commit updates the three doc
references that hardcoded the old URL.

SESSION_HANDOFF.md's "Canonical remote" section now names the new
URL and explains the rename for future readers, including the note
that local clone directories may still be named Rusty_Solitare —
that's a local-only name and works fine, only the GitHub repo URL
changed.

docs/SESSION_HANDOFF.md (older snapshot, unchanged otherwise) gets
its single URL line corrected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:36:06 +00:00
funman300 160637d1c8 docs: update remote URL reference to github.com/funman300/Rusty_Solitare
CI / Test & Lint (push) Failing after 8s
CI / Release Build (push) Has been skipped
Mirrors the move of the canonical remote from git.aleshym.co to
GitHub. The git remote itself was switched via 'git remote set-url
origin'; this updates the one stale URL in docs/SESSION_HANDOFF.md
that named the old host.
2026-05-01 17:11:55 +00:00
funman300 41d75b50de feat/fix/perf(engine,data,assetgen): ambient audio, sync bug fixes, hot-path cleanup
**ambient_loop.wav (task 5)**
- solitaire_assetgen: add ambient_loop() synthesizer — 5 s seamless loop,
  55 Hz drone with 2nd/3rd harmonics, 0.2 Hz LFO breath, 16-bit mono 44100 Hz
- audio_plugin: load ambient_loop.wav via include_bytes!() replacing the
  card_flip.wav placeholder; decouple start_ambient_loop() from SoundLibrary

**sync bug fixes (task 11)**
- sync_plugin: LocalOnlyProvider returning UnsupportedPlatform now sets
  SyncStatus::Idle instead of displaying a misleading "Sync not configured" error
- sync_client: extract_pull_body / extract_push_body now return SyncError::Auth
  only for HTTP 401/403; all other non-2xx statuses return SyncError::Network
- sync_plugin: push_on_exit now logs a warn! on failure instead of silently
  discarding the result

**hot-path performance (task 12)**
- card_plugin: card_positions() now returns &Card references (lifetime-bound to
  GameState) instead of owned Card clones — eliminates 52 Card clones per
  sync_cards() call (runs every animation frame)
- input_plugin: card_position() takes &PileType instead of PileType, eliminating
  PileType copies at every drag hit-test call site
- animation_plugin: eliminate intermediate AnimSpeed clone in handle_win_cascade()

**docs (tasks 11, 13)**
- docs/sync_test_runbook.md: manual test runbook for cross-machine sync
- docs/android_investigation.md: cargo-mobile2 port investigation and effort estimate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:51:58 +00:00
funman300 13b428b81c feat(engine): first-run onboarding banner
OnboardingPlugin spawns a centered welcome banner at PostStartup
when Settings.first_run_complete is false. Any key or mouse
press dismisses it, sets the flag, and persists settings.json
so returning players never see it again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 23:14:10 -07:00
funman300 9d0f9478b2 feat(data,engine): persistent Settings + SFX volume hotkeys
- solitaire_data::Settings { sfx_volume, first_run_complete } with
  atomic JSON persistence and clamping sanitizer.
- SettingsPlugin (engine): [ / ] adjust SFX volume by 0.1, clamped;
  persists on change; emits SettingsChangedEvent. No-op at rails.
- AudioPlugin applies sfx_volume to kira's main track at startup
  and on every change so live tweaks take effect without restart.
- Brief "SFX: N%" toast on each change. Help cheat sheet updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 23:08:20 -07:00
funman300 b720588687 feat(engine): MoveRejectedEvent + PausePlugin (Esc)
- New MoveRejectedEvent fires from end_drag when the cursor is over
  a real pile but the placement is illegal. AudioPlugin plays
  card_invalid.wav on it.
- New PausePlugin + PausedResource: Esc toggles a full-window
  overlay and the flag. tick_elapsed_time and advance_time_attack
  skip work while paused. Help cheat sheet updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:56:35 -07:00
funman300 adacdf533c feat(engine,assetgen): synthesized SFX + kira AudioPlugin
- New solitaire_assetgen crate with gen_sfx binary: synthesizes
  five 44.1kHz mono 16-bit PCM WAVs (flip/place/deal/invalid/fanfare)
  from an LCG noise source + sine/square synths. Output committed
  under assets/audio/.
- AudioPlugin (engine): embeds the WAVs via include_bytes!, decodes
  once with kira::StaticSoundData, plays on Draw / Move / NewGame /
  GameWon events. card_invalid is loaded but unused — wiring it
  needs a MoveRejectedEvent.
- AudioManager kept on the main thread (NonSend) since cpal is !Send
  on some platforms; degrades gracefully if no audio device present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:48:58 -07:00
funman300 7dfbff45d1 feat(engine): add HelpPlugin (H/?) and Challenge cleared toast
- HelpPlugin: full-window cheat sheet listing every keybinding,
  toggled with H or ?. Three unit tests cover open/close/slash.
- AnimationPlugin: ChallengeAdvancedEvent now surfaces as a
  3-second "Challenge N cleared!" toast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:39:08 -07:00
funman300 193410200e feat(engine,core): add Time Attack mode + unlocks panel
- Core: GameMode::TimeAttack variant (no scoring/undo changes — session marker only)
- Engine: TimeAttackPlugin with TimeAttackResource, TimeAttackEndedEvent,
  T hotkey (gated to level >= 5), auto-deal on win, summary toast
- Engine: Stats overlay (S) gains an Unlocks subsection (card backs /
  backgrounds, sorted/deduped) and a live Time Attack panel while active

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 17:27:53 -07:00
funman300 294f6fe9d4 docs: mark Phase 6 part 4b (Challenge mode + level-5 gate) complete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 17:18:50 -07:00
funman300 09d62f4255 docs: mark Phase 6 part 4a (elapsed time + Zen mode) complete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:16:04 -07:00
funman300 0fdfbced6d docs: mark Phase 6 part 3 (completion toasts + progression panel) complete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:28:26 -07:00