Touch layout (USE_TOUCH_UI_LAYOUT) restructures the bottom action bar
to five buttons: an enlarged Undo / Draw / Hint trio (96x64px targets,
1.35x labels) between compact Menu and Pause. Draw is new — it fires
the same DrawRequestEvent as tapping the stock, so the most frequent
action no longer needs a reach to the top of a tall folded screen.
Help, Modes, and New Game leave the touch bar (they live in Menu ->
System, the Home grid, and Home's hero respectively). Desktop keeps
the seven-button bar unchanged (decision 5: touch-only).
Holding Undo now steps back repeatedly after a 0.45s delay (5.5/s),
each step through the normal request path so the scoring penalty
applies. New self-ambiguous DrawRequestWriters set keeps the ambiguity
gate at zero with the fourth DrawRequestEvent writer.
6 new hud_plugin tests; workspace suite + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gitea actions cache on this instance stores caches (1.85 GB saves
confirmed in run 597) but never restores them — every run back through
run 584+ logs 'No cache found' even for exact keys saved an hour
earlier, including master-to-master. Every CI run has therefore been a
full cold build (~37 min), plus ~4 min tarring a cache nobody reads.
- Point CARGO_TARGET_DIR at a persistent path on the rust-host runner
(host executor — filesystem carries over between runs), with a 40 GiB
prune guard. Warm runs drop to minutes without touching the broken
cache API.
- Drop Swatinem/rust-cache (pure overhead until the server is fixed).
- Split cargo fmt --check into a seconds-long fmt job gating the heavy
test job, so a formatting slip can't burn a 35-minute build again
(run 600).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructures the Home overlay per docs/ui-redesign-2026-07.md Phase B:
- Continue card (mode / elapsed / score) when a game is in progress;
clicking returns to the table
- Hero New Game replays the persisted Settings::last_mode (new field,
serde-default Classic) with one tap; locked modes fall back to Classic
- Deal options (draw 1/3, winnable-only, difficulty tiers) move into a
disclosure under the hero — off the Home top level (decision 2)
- Compact symmetric 2x3 mode grid; descriptions on wide viewports only
- Stats strip moves to the bottom (right pane on wide)
- Two-pane body on wide viewports (>= 1000 logical px) via new
ui_modal::spawn_modal_sized (default card stays 720 px)
- Cancel becomes "Back to table" and only renders while a live
(un-won) game exists
10 new home_plugin tests; all 936 engine tests + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cycle regression gate still read payload.moves, which schema v4
removed in favour of recording.instructions, so every game reported
replay_history_mismatch:0/N and games_with_issues tripped the
--require-zero-issues gate on master (runs 587/594/596).
Verified locally: 12-game gate run reports 0 issues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #170 changed the web replay payload (moves list -> embedded session
recording) but missed these Playwright specs, which only run on master
pushes and so failed post-merge. Assertions now match the v4 shape:
schema_version 4, recording.initial_state present, moves at
recording.instructions.
Verified locally against a real server with freshly built wasm bundles:
full suite 18/18 green, including the five play_canvas specs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GameState's save serde had the same latent flaw PR #170 removed from
replays: v5 persisted seed + saved_moves and re-dealt the board from the
seed on load, so any RNG or upstream upgrade that shifts the seed->deal
mapping would invalidate every in-progress save (graceful error, but the
player loses their game). v6 persists the existing SessionRecording
payload (upstream card_game session serde: config + dealt board +
instructions) instead; seed stays as presentation metadata only.
- v4/v5 files still load through the legacy seed path and are rewritten
as v6 on the next save; v6 files load from the recording and replay
with per-instruction validation, so tampered or corrupt files surface
an error rather than a silently wrong board
- saved_moves() removed (dead once serialisation reads recording())
- tests: v6 round-trip via storage, seed-corruption immunity, v5 legacy
load, missing-recording rejection, invalid-history rejection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replays previously persisted only seed + moves and re-dealt the board
from the seed at playback time, so any change to the seed->deal mapping
(RNG bumps, upstream upgrades) silently invalidated every existing
replay. Schema v4 instead embeds a SessionRecording - the upstream
card_game Session serde ({config, initial_state, instructions}) - so
playback rebuilds the exact recorded board; seed/draw_mode/mode remain
caption metadata only.
- core: SessionRecording newtype delegating to Session<Klondike> serde;
GameState::recording() / from_recording(); from_instructions_unchecked
fixture helper; serde_json added to dev-deps (tests only)
- data: Replay v4 (recording replaces moves); v1-v3 files rejected by
the existing version gate
- engine: win-recording and sync upload freeze game.recording();
playback rebuilds from the recording; Playing carries the extracted
move list (+ Box<Replay> for clippy large_enum_variant)
- wasm: replay_export() builds the full v4 upload payload so JS never
hand-assembles it (the old game.js path hardcoded schema_version: 2
and corrupted u64 seeds via Math.round); ReplayPlayer::from_json
enforces schema_version == 4 with a descriptive error
- web: game.js/play.html use replay_export; replay.js surfaces player
construction errors in the caption instead of dying silently
- server: mode validation accepts data-carrying GameMode variants
(Difficulty uploads previously 400'd against the String field)
Both replays on prod are May-era v1 rows with empty move lists - every
shared replay was already unplayable; the viewer now says why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three items the multi-agent sweep flagged, now removed with user
approval (§8 for the solitaire_sync changes):
- WinCascadePlugin: never registered; handle_win_cascade in
AnimationPlugin is the live win cascade and builds its own targets.
Its now-orphaned helpers (win_scatter_targets, cascade_delay,
WIN_CASCADE_INTERVAL_SECS) had no callers outside their own tests
and go with it.
- SyncCompleteEvent: written by the pull-completion system, zero
readers — UI reads SyncStatusResource instead.
- solitaire_sync::ApiError: unused by client and server; the merge_at
crate-root re-export goes too (merge::merge_at stays for the merge
module's own use).
ARCHITECTURE.md updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wasm bundles in solitaire_server/web/pkg/ are no longer tracked. The
web-wasm-rebuild workflow (which rebuilt them in CI and committed them
back to master) is gone; instead:
- solitaire_server/Dockerfile gains a wasm-builder stage that runs
build_wasm.sh with the same pinned toolchain (wasm-bindgen 0.2.120,
wasm-pack 0.14.0, binaryen 130) and the runtime image copies pkg/
from it — the image build is now the artifacts' single source of truth.
- web-e2e builds the wasm before Playwright runs, and its trigger paths
now include the wasm-feeding crates it actually tests.
- docker-build triggers on solitaire_data/** and build_wasm.sh too, so
every wasm-affecting change redeploys.
- Self-hosters serving /web or /play from a source checkout run
./build_wasm.sh once (script header and ARCHITECTURE.md updated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double-click/tap auto-move previously tried the run's top card alone
before the full run headed by the clicked card, so a top card with a
foundation move hijacked the intended whole-stack move. Both handlers
now share auto_move_for_run: a lone top card goes foundation-first, a
multi-card run moves whole to a tableau or not at all. The double-click
key is now the clicked card, so two clicks on different cards of the
same stack no longer register as a double-click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compiler-verified via RUSTFLAGS=--force-warn dead_code plus workspace-wide
reference greps; three parallel audit agents covered the engine crate, the
other eight crates, and Copilot commit-message-vs-diff drift.
Removed:
- replay_overlay/input.rs: 19 orphaned twins (~950 lines) of items also
defined in mod.rs — the glob re-export made the mod.rs copies win and
the file-level #![allow(dead_code)] hid the corpses. The live keyboard/
button handlers and ReplayScrubKeyHold stay; the allow is retired.
- retarget_animation (never called; doc examples were its only refs)
- ScanThemesRequestEvent (never registered/written/read; its doc claimed
a handle_scan_themes consumer that does not exist)
- _VEC3_REFERENCED workaround const + now-unneeded Vec3 import
- solitaire_data: load_stats/save_stats/time_attack_session_with_now
default-path wrappers (the _from/_to variants are the live API) and
surplus re-export names (settings MIN/MAX bounds, token loaders)
- solitaire_core: Session re-export (no external consumer)
- solitaire_wasm: ReplayPlayer::is_finished (no JS caller)
- solitaire_app: build_app wrapper (real entry is run())
Doc fixes:
- audio_plugin: WAV count 5→7, add FoundationCompletedEvent table row,
drop bogus 'placeholder' label, bevy_kira_audio→kira
- ToastVariant::Warning: variant is live (5 writer plugins); dropped the
stale allow(dead_code) and its 'currently unused' comment
Deliberately kept: Spider module (staged forward work), WinCascadePlugin
(documented alternative cascade, pending owner decision), SyncCompleteEvent
and solitaire_sync ApiError/merge_at (§8 change-controlled, flagged to owner).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 38e4c03 (shipped in v0.40.0) switched Android's focused_mode to
reactive_low_power(100 ms) on the premise that every animation tick
system writes RequestRedraw while it has active work. The writers were
never actually added — the diff only contained the WinitSettings change,
imports, and add_message registrations. Result: with no touch input,
nothing wakes the winit loop during a card slide except the 100 ms
fallback ceiling, so animations render at ~10 fps on Android. Desktop
and web keep Continuous mode, which is why only Android was affected
(reported by Rhys the day v0.40.0 shipped; confirmed absent on v0.39.1).
Adds the missing MessageWriter<RequestRedraw> to all seven systems the
original commit message named:
- advance_card_animations (CardAnimationPlugin)
- advance_card_anims (AnimationPlugin — deal/win cascade/slides)
- tick_shake_anim, tick_settle_anim, tick_foundation_flourish
(FeedbackAnimPlugin)
- drive_toast_display (AnimationPlugin — toast countdown)
- drive_auto_complete (AutoCompletePlugin — step-interval keepalive)
Each writes one RequestRedraw per frame while active work exists
(including delay phases, which also need per-frame ticks). Regression
test asserts an active CardAnimation emits RequestRedraw and an idle
board does not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase G of docs/ui-redesign-2026-07.md. The win modal now leads with
actions and reads the stats recap quietly below them:
- Play Again: primary, full-row, Enter accelerator unchanged; still
fires NewGameRequestEvent::default() (same mode; deal options from
Settings) so the rematch is one tap.
- Watch Replay / Share Replay: secondary pair reusing the global
stats_plugin markers (WatchReplayButton / CopyShareLinkButton), so
both act on the just-won replay — SelectedReplayIndex snaps to 0 on
every win. This also fixes the old win-modal handler picking
replays.last(), which after the newest-first history refactor was
the OLDEST replay, not the newest.
- Watch closes the celebration overlay (new close_overlay_on_watch_-
replay system); Share keeps it open and relies on the existing
copy-feedback toasts.
- Stats recap (score breakdown reveal, time, XP, achievements) moves
below the actions; the Time line drops from headline/primary to
body/secondary styling.
Tests: 4 new (action presence, Play Again close+request, Watch closes,
Share keeps open) on a manual-clock fixture that steps the 0.5 s
celebration delay deterministically. Workspace suite green, workspace
clippy -D warnings clean, fmt applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C dismissal audit: Esc / scrim-tap / Done must behave the same
on every modal. Stragglers found and fixed — none of these had any
Esc path (pause's toggle guard swallowed the key while they were
open):
- Settings: Esc clears SettingsScreen, gated on being the topmost
modal so a stacked sync-setup / theme-store dialog owns Esc
- Help: Esc closes alongside F1/Done (the code comment already
claimed an Esc path existed — now it does)
- Leaderboard: Esc closes when topmost; the display-name dialog
stacked above it now Esc-cancels like sync-setup's dialog
- Theme store: Esc closes (always topmost when open)
Scrim-tap opt-ins are unchanged — ui_modal documents which modals
deliberately stay non-dismissible on outside clicks.
Tests: escape_closes_help_screen, escape_closes_settings_screen_flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C of docs/ui-redesign-2026-07.md. The Modes row is gone — Home
owns mode selection, so the popover's Play section carries a Home row
firing the new ToggleHomeRequestEvent (read by toggle_home_screen
alongside the existing M accelerator). Section headers are quiet
caption-size labels inside the existing panel widget, not a new
widget. The action-bar Modes button and its popover are untouched
(their removal is Phase B territory when Home gains hierarchy).
- MenuOption: Modes variant replaced by Home; rows grouped Play (Home)
· You (Profile, Stats, Achievements) · Community (Leaderboard) ·
System (Settings, Help)
- handle_menu_option_click no longer chains into spawn_modes_popover
- Tests: tooltip sweep updated (7 rows still), new
toggle_home_event_opens_home_screen covers the popover's open path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>