Commit Graph

808 Commits

Author SHA1 Message Date
funman300 de7ae16830 fix(onboarding): delay first-run modal until splash screen despawns
Build and Deploy / build-and-push (push) Successful in 4m35s
Web E2E / web-e2e (push) Successful in 4m25s
OnboardingPlugin previously used PostStartup which fires before the
first Update tick — guaranteeing the onboarding modal and the launch
splash (MOTION_SPLASH_TOTAL_SECS = 1.6 s) overlap for the entire
splash duration. The splash sits at Z_SPLASH (the highest UI z-index),
so the two screens fought visually and the user saw a confusing frozen
composite before the splash faded out.

Fix: move spawn_if_first_run to Update and gate it on
`splashes.is_empty()` (no SplashRoot entity alive). A Local<bool>
ensures the spawn fires at most once per session. Cost: ~one frame of
latency after the splash clears, which is imperceptible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:59:58 -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 763fdb486f fix(input): hit-test deck at correct position; accept waste click too
Build and Deploy / build-and-push (push) Successful in 4m36s
pile_positions[KlondikePile::Stock] stores the waste column position
(col_x(1)).  card_plugin renders the face-down deck one column to the
left (col_x(0) = Tableau1 x) via `base.x -= tableau_col_step`.

handle_stock_click and handle_touch_stock_tap were using pile_positions
[Stock] directly, so the click hotspot was on the waste card (right
column) instead of the deck (left column).  Result: clicking the
visible face-down deck did nothing, while clicking the waste pile
triggered draw.

Fix: compute deck_pos = Vec2::new(tableau1.x, waste_pos.y) and hit-test
both the deck column AND the waste slot.  Accepting waste clicks matches
standard Klondike UX where either card acts as the draw trigger.

Touch tap handler receives the same fix.

Also rebuild canvas_bg.wasm with the corrected engine source and
-O2 optimisation (replacing the previous -Oz that caused grey screen).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:39:01 -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 baf524ec75 fix(web): rebuild Bevy canvas WASM; add SolitaireGame interactive API
Grey screen fix (canvas_bg.wasm):
- Rebuilt Bevy WASM from refactored solitaire_core that removes the
  per-game KlondikeAdapter field from GameState. The old binary was
  built with wasm-opt -Oz; the large adapter allocation pattern appears
  to trigger an over-aggressive wasm-opt optimisation that corrupts
  Bevy's render pipeline, causing a permanent grey screen on /play.
- build_wasm.sh: change wasm-opt -Oz → -O2. Speed-optimised level avoids
  the size-focused transforms that miscompile Bevy's deep render stacks.

solitaire_core refactoring:
- game_state.rs: remove adapter: KlondikeAdapter field; use static
  KlondikeAdapter::config_for() instead of a per-instance allocation.
  Gate test_pile_state behind #[cfg(feature = "test-support")] so
  production builds carry no test-only heap state.
  Add instruction_history() public accessor (delegates to saved_moves()).
- card.rs: add Card::new(), face_up(), face_down() const constructors
  for more ergonomic test and wasm code.
- pile.rs, solver.rs: cargo fmt.

solitaire_wasm interactive API:
- lib.rs: add SolitaireGame wasm-bindgen struct with draw(), move_cards(),
  undo(), auto_complete_step(), serialize(), from_saved() — the full
  player-action surface used by game.js.
  Add DebugSnapshot, DebugMove, DebugInvariantReport structs and
  debug_snapshot(), debug_legal_moves(), debug_apply_move_json()
  methods for e2e test automation (window.__FERROUS_DEBUG__ bridge).
  Add replay_moves() to export the current game as a Replay v2 payload.
- solitaire_wasm.js + solitaire_wasm_bg.wasm: rebuilt with new API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:21:20 -07:00
funman300 9ff0585454 fix(ci): remove Quaternions registry auth; add canvas WASM drift guard
Dockerfile:
- Drop --mount=type=secret,id=cargo_token: the Quaternions private
  registry has been migrated to the public Cargo.io path so the build
  secret is no longer needed. Removes the requirement for CI_TOKEN to
  carry registry credentials.

CI workflow (docker-build.yml):
- Add solitaire_wasm/** and solitaire_web/** to the push-trigger paths
  so changes to either WASM crate actually fire the build job.
- Add wasm drift check for solitaire_wasm artifacts (solitaire_wasm.js,
  solitaire_wasm_bg.wasm) — exits 1 if solitaire_wasm/ or solitaire_core/
  changed without updating the committed pkg files.
- Add hard canvas drift check: solitaire_web/ changes MUST update
  canvas_bg.wasm or the deploy gets a stale Bevy binary.
- Add advisory notice for solitaire_engine/ / solitaire_core/ changes
  that omit a canvas_bg.wasm rebuild (non-blocking; formatting commits
  should not fail CI).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 12:20:56 -07:00
funman300 64f975ed6d fix(ux): 14 cross-platform UX/UI fixes from 500-game audit
Web client (game.js):
- Restart game timer after undo exits auto-complete sequence
- Pause timer while browser tab is hidden (visibilitychange)
- Validate URL seed — NaN / negative falls back to randomSeed()
- Guard onBoardClick/onBoardDblClick during win (snap.is_won)
- Delay win overlay 320 ms so last card CSS transition finishes
- Force reflow in flashIllegal() to restart shake on rapid re-trigger

Android (safe_area.rs):
- Preserve last-known insets on app resume instead of zeroing them;
  eliminates double layout flash on every foreground cycle

All clients — Bevy engine:
- Radial menu: clamp icon anchors to viewport bounds so icons are
  never placed off-screen on narrow phones
- Auto-complete: deactivate state.active when is_auto_completable
  goes false (undo mid-sequence) to stop perpetual background retry
- Touch selection: gate highlight rebuild on is_changed() — was
  despawning/respawning entities every frame unnecessarily
- Input: fire "Tap a pile to move" InfoToast on first tap in
  TapToSelect mode; document cursor_world 1:1 viewport invariant
- Drag threshold: raise desktop from 4 → 6 px to prevent accidental
  drags from cursor jitter on HiDPI displays

Desktop / Android (solitaire_app):
- Call cleanup_orphaned_tmp_files() at startup to remove .tmp files
  left by crashes between atomic write and rename

Design clarification (klondike_adapter.rs):
- Doc comment: Draw-1 recycling is penalty-only by design (never
  blocked) to avoid creating unwinnable positions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:23:52 -07:00
funman300 20e5222148 fix(engine): send confirmed:true from game-over screen New Game handlers
Build and Deploy / build-and-push (push) Successful in 4m51s
The 'No Moves Available' dialog's New Game button and keyboard shortcut
were firing NewGameRequestEvent::default() (confirmed: false). When the
player has made moves, handle_new_game sees needs_confirm = true, then
hits the scrims.is_empty() guard — which is false because the GameOver-
Screen itself is a ModalScrim — and silently returns without starting a
new game or showing the confirm dialog.

Fix: set confirmed: true in both handle_game_over_input (N/Escape key)
and handle_game_over_button_input (click). The game is already stuck so
the abandon-confirmation guard does not apply, as the doc comment on the
button handler has always said.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:26:07 -07:00
funman300 44e90ff582 fix(ci): pass Quaternions registry token as Docker build secret
Build and Deploy / build-and-push (push) Successful in 4m39s
cargo fetch --locked was failing with "failed to parse manifest" because
.cargo/config.toml (which registers the Quaternions sparse index) was
never copied into the build image, and the registry's auth token was
never supplied.

Changes:
- COPY .cargo/config.toml into the builder stage so Cargo knows the
  Quaternions registry URL.
- Replace bare `cargo fetch` and `cargo build` with
  `--mount=type=secret,id=cargo_token` variants that set
  CARGO_REGISTRIES_QUATERNIONS_TOKEN from the mounted secret — token
  never appears in image layers or docker history.
- Workflow: pass CI_TOKEN as the `cargo_token` build secret.
- Add solitaire_engine/** and solitaire_server/Dockerfile to trigger
  paths so engine changes and Dockerfile edits kick off rebuilds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:58:25 -07:00
funman300 0bae839e3b fix(wasm): gate wasm32-only imports behind cfg, add binaryen wasm-opt pass
Build and Deploy / build-and-push (push) Failing after 1m12s
- Gate `Startup` and `user_theme_dir` imports in theme/registry.rs
  behind `#[cfg(not(target_arch = "wasm32"))]` — they are only used
  in the non-wasm code path, eliminating two unused-import warnings
  in the WASM release build.
- Rebuild canvas_bg.wasm and solitaire_wasm_bg.wasm with wasm-opt -Oz
  (binaryen v129); canvas_bg.wasm drops from 57 MB → 30 MB.
- Add solitaire_web/Cargo.toml stub to server Dockerfile so
  `cargo fetch --locked` resolves all workspace members.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:53:36 -07:00
funman300 c68cf96488 fix(web): add WgpuSettingsPriority::WebGL2 for Chromium shader compatibility
Build and Deploy / build-and-push (push) Failing after 43s
Without this setting, wgpu's naga SPIR-V→GLSL translator uses features
unsupported by ANGLE (Chromium's WebGL2 implementation): storage buffers,
tight inter-stage component limits, etc. ANGLE rejects these shaders with
a fatal "Shader translation error" and a context-lost event.

WgpuSettingsPriority::WebGL2 constrains naga to emit GLES 300es-compatible
GLSL (same limits as WebGL2 spec: no storage buffers, max 31 inter-stage
components, max 255-byte vertex stride). Firefox was already permissive
enough to work without this; Chromium required it.

Result: game renders correctly in both Chromium (ANGLE/SwiftShader) and
Firefox (native WebGL2), with zero JS errors in both environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:24:27 -07:00
funman300 a92ac066a6 fix(web): resolve wasm32 runtime panics; game boots and renders in Firefox
Build and Deploy / build-and-push (push) Failing after 1m6s
Fixes found while testing the Bevy WASM build in a real browser:

1. chrono wasmbind: add `wasmbind` feature to workspace chrono dep so
   Local::now()/Utc::now() use js-sys::Date on wasm32 (previously
   fell through to std::time::SystemTime which panics on wasm32).

2. std::time::SystemTime: replace all remaining direct SystemTime::now()
   calls (4 sites across game_plugin, difficulty_plugin, time_attack_plugin,
   solitaire_data/storage) with chrono::Utc::now() which is wasm32-safe.

3. user_dir: return empty PathBuf (instead of panicking) when data_dir()
   is None on wasm32; there is no filesystem in the browser so user themes
   are unsupported and a benign empty path is correct.

4. ThemeRegistryPlugin: gate build_registry_on_startup to non-wasm32
   (the filesystem scan for user themes has nothing to scan in the browser;
   only the bundled embedded themes are available).

5. AssetMetaCheck::Never: configure AssetPlugin in solitaire_web to skip
   `.meta` sidecar fetches — we don't ship .meta files, so the default
   AssetMetaCheck::Always produced a 404 flood on every card/background asset.

Result: `http://localhost:<port>/play` boots in Firefox with zero errors
and renders the full Bevy game — home screen, onboarding modal, HUD all
visible. Assets load correctly from /assets/. Chromium has a separate
wgpu-27/ANGLE/GLES shader translation bug (not in our code); Firefox works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:16:19 -07:00
funman300 f464aab543 fix(web): clean up wasm32 build warnings and wire /play route to Bevy canvas
Build and Deploy / build-and-push (push) Failing after 44s
- solitaire_data/sync_client.rs: fix SyncPayload/SyncResponse import split
  (SyncResponse is needed by LocalOnlyProvider which compiles on wasm32)
- solitaire_engine/assets/sources.rs: cfg-gate AssetApp/AssetSourceBuilder
  imports (only used in the non-wasm FileAssetReader block)
- solitaire_engine/auto_complete_plugin.rs: cfg-gate AUTO_COMPLETE_CHIME_VOLUME
- solitaire_engine/daily_challenge_plugin.rs: cfg-gate Task/AsyncComputeTaskPool
  imports and DailyChallengeTask struct (server fetch systems are non-wasm only)
- solitaire_engine/resources.rs: cfg-gate std::sync::Arc (TokioRuntimeResource
  is non-wasm only)
- solitaire_engine/settings_plugin.rs: cfg-gate ScanThemes variant, pill_button,
  and their match arms; fix refresh_registry import placement
- solitaire_server/src/lib.rs: point /play route at play.html (Bevy canvas);
  keep /play-classic serving game.html during transition period
- build_wasm.sh: add --no-typescript to wasm-bindgen call for canvas build
- solitaire_server/web/pkg: add canvas.js + canvas_bg.wasm build artifacts

wasm32 build and native clippy --workspace -D warnings both clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:55:39 -07:00
funman300 835a48fe9d feat(web): add solitaire_web Bevy WASM build targeting play.html canvas
Build and Deploy / build-and-push (push) Failing after 58s
Adds a new `solitaire_web` crate that compiles the full `solitaire_engine`
to `wasm32-unknown-unknown` and renders to a `<canvas id="bevy-canvas">`
element in `play.html` — the same ECS code path as desktop and Android.

Changes to enable the WASM target:
- .cargo/config.toml: add wasm32-unknown-unknown rustflags for getrandom
- Workspace Cargo.toml: add solitaire_web member
- solitaire_data/Cargo.toml: gate tokio/reqwest/dirs/keyring to non-wasm
- solitaire_data/src: add wasm32 branch to data_dir() (returns None);
  cfg-gate sync_client network types, auth_tokens, matomo_client
- solitaire_engine/Cargo.toml: gate tokio/reqwest/kira/arboard/dirs/zip
  to non-wasm (mio/cpal/arboard don't compile for wasm32-unknown-unknown)
- solitaire_engine/src/lib.rs: cfg-gate module declarations and re-exports
  for analytics, audio, sync, sync_setup, avatar, leaderboard plugins
- solitaire_engine/src/core_game_plugin.rs: cfg-gate plugin registrations
  that require TokioRuntime (audio, sync, analytics, leaderboard, avatar)
- solitaire_engine/src/resources.rs: cfg-gate TokioRuntimeResource
- solitaire_engine/src/game_plugin.rs: cfg-gate std::fs::remove_file (x10)
- solitaire_engine/src/theme/mod.rs: cfg-gate importer module (uses dirs+zip)
- solitaire_engine/src/settings_plugin.rs: cfg-gate theme ZIP import UI
- solitaire_engine/src/assets/sources.rs: cfg-gate FileAssetReader/user_theme_dir
- solitaire_engine/src/auto_complete_plugin.rs: cfg-gate audio system
- solitaire_engine/src/daily_challenge_plugin.rs: cfg-gate server fetch
- solitaire_engine/src/hud_plugin.rs: cfg-gate AvatarResource import
- solitaire_engine/src/profile_plugin.rs: cfg-gate AvatarResource import
- solitaire_server/web/play.html: minimal HTML canvas shell
- solitaire_web/: new crate (Cargo.toml + src/lib.rs)
- build_wasm.sh: add Bevy WASM build step (cargo + wasm-bindgen + wasm-opt)

All tests pass; clippy --workspace -- -D warnings clean; native build
(solitaire_engine, solitaire_app) unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:46:45 -07:00
funman300 9260ca7994 refactor: migrate PileType → KlondikePile across core/wasm/engine
Build and Deploy / build-and-push (push) Failing after 1m24s
- Replace PileType with typed KlondikePile (Foundation/Tableau variants)
  throughout solitaire_core, solitaire_wasm, and solitaire_engine;
  ReplayMove now uses SavedKlondikePile for serialisation stability
- Split replay_overlay.rs into replay_overlay/ module (mod, format,
  input, update, tests) for maintainability
- Add klondike dep to solitaire_engine and solitaire_data Cargo.toml
- Add TestPileState infrastructure to game_state.rs for engine unit tests
- Rebuild solitaire_wasm pkg (js + wasm artefacts updated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:13:35 -07:00
funman300 ca612f51f1 Revert "refactor(core): split game_state.rs into submodule directory"
Build and Deploy / build-and-push (push) Failing after 50s
This reverts commit dba154cf92.
2026-05-29 18:51:59 -07:00
funman300 dba154cf92 refactor(core): split game_state.rs into submodule directory
Build and Deploy / build-and-push (push) Failing after 44s
1692-line monolith → 4 focused files:
- mod.rs (580): types, constructors, instruction mapping, core game actions
- serde_impl.rs (119): PersistedGameState + Serialize/Deserialize/PartialEq impls
- hints.rs (141): auto-complete detection and move-hint queries
- tests.rs (866): all 118 unit tests

No logic changes; all tests pass; clippy clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 18:24:47 -07:00
funman300 258abd198e chore(core): remove dead card_to_kl / suit_to_kl / rank_to_kl helpers
Build and Deploy / build-and-push (push) Failing after 56s
These were scaffolded for a future KlondikeState::from_piles() path
that never materialised. card_from_kl (used by sync_piles_from_session)
is retained; suit_from_kl / rank_from_kl are narrowed to pub(crate).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 18:11:54 -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 6309d3325f fix(engine): auto-complete delay + right-click shake on no legal move
Closes #80: add AUTO_COMPLETE_INITIAL_DELAY (0.75 s) before the first
auto-complete move fires. Previously cooldown was 0.0, causing the
sequence to hijack the board the same frame the condition was met.

Closes #81: fire MoveRejectedEvent in radial_open_on_right_click when
the right-clicked card has no legal destinations, so the shake
animation and invalid-move sound play consistently on desktop/web.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 18:01:34 -07:00
funman300 862f7e4b48 chore(core): delete deck.rs and scoring.rs
Build and Deploy / build-and-push (push) Failing after 1m18s
- deck.rs (193 lines) — Deck/deal_klondike replaced by Klondike::with_seed()
- scoring.rs (152 lines) — scoring fns superseded by KlondikeAdapter; move
  compute_time_bonus to klondike_adapter.rs, update win_summary_plugin import
- Remove rand dep from solitaire_core (only used by deck.rs)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 17:42:30 -07:00
funman300 6496e130f3 feat(core): Step 2 — replace pile management with Session<Klondike>
Build and Deploy / build-and-push (push) Failing after 29s
- Delete rules.rs (228 lines) — move validation now handled by klondike engine
- Delete SolverState DFS from solver.rs (~900 lines) — replaced by session.solve()
- Rewrite GameState::new_with_mode() using Klondike::with_seed() (removes deck.rs dep)
- Rewrite move_cards/draw/undo to use Session<Klondike> as move executor
- Remove internal undo_stack (VecDeque<StateSnapshot>) — session owns history
- Sync piles from KlondikeState after each move via sync_piles_from_session()
- Update engine layer (game_plugin, input_plugin, card_plugin, etc.) to new API
- Net: 821 insertions, 3872 deletions (-3051 lines)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 17:31:09 -07:00
funman300 d4796fa252 feat(core): integrate klondike v0.3.0 / card_game v0.4.0 — solver + serde newtypes
Build and Deploy / build-and-push (push) Failing after 29s
Step 6: replace 767-line DFS seed-solver with Session<Klondike>::solve().
- try_solve_with_first_move() now delegates to card_game::Session::solve()
  with solve_moves_budget/solve_states_budget from SolverConfig
- Maps Ok(Some) → Winnable, Ok(None) → Unwinnable, Err → Inconclusive
- try_solve_from_state() retains the DFS (pile mapping pending, step 2)
- Removed dead SolverState::initial() — no longer needed for seed path
- Updated tests: session solver returns no Unwinnable in 0..500 range
  (all non-Winnable deals are Inconclusive); updated engine seed-retry test

Step 7: SavedInstruction serde newtypes in klondike_adapter.
- SavedInstruction mirrors KlondikeInstruction with Serialize+Deserialize
- Sub-types: SavedDstFoundation, SavedDstTableau, SavedKlondikePile,
  SavedKlondikePileStack, SavedTableauStack, SavedTableau, SavedFoundation,
  SavedSkipCards — all with serde derives
- From<KlondikeInstruction> for SavedInstruction (infallible)
- TryFrom<SavedInstruction> for KlondikeInstruction (InvalidSavedInstruction
  on out-of-range u8 values)
- InvalidSavedInstruction error type via thiserror

Also: chore(deps): bump klondike to v0.3.0, card_game to v0.4.0 (Cargo.toml/lock)

All 1399 tests pass; clippy clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 15:43:32 -07:00
funman300 57c4b5aacf feat(core): card/pile conversion utils and GameMode-aware scoring (steps 2-prep, 5)
Build and Deploy / build-and-push (push) Failing after 55s
Step 2 prep — card_game dep + type-conversion utilities:
- Add card_game = "0.3.0" (registry Quaternions) to workspace + core
- suit_to_kl / suit_from_kl, rank_to_kl / rank_from_kl
- card_to_kl (drops id, Deck1), card_from_kl (reconstructs stable id
  from Clubs-first suit×13+rank ordering matching deck.rs)
- Ready to wire into KlondikeState pile projection once upstream
  adds KlondikeState::from_piles()

Step 5 — GameMode-aware scoring in the adapter:
- score_for_move_with_mode, score_for_flip_with_mode (return 0 in Zen)
- apply_undo_score (static, handles Zen + −15 penalty + clamp)
- score_for_recycle_with_mode (return 0 in Zen)
- game_state.rs: all inline GameMode::Zen checks replaced with
  adapter calls; adapter is now the single source of truth for
  "what score does this action give in this mode"

192 tests pass; clippy -D warnings clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:38:41 -07:00
funman300 f1914b4398 feat(core): add klondike v0.2.0 dep and KlondikeAdapter (integration steps 1, 3, 4)
Build and Deploy / build-and-push (push) Failing after 1m0s
Step 1 — Cargo & registry:
- Add .cargo/config.toml with Quaternions sparse registry
  (https://git.aleshym.co/api/packages/Quaternions/cargo/)
- Add klondike = "0.2.0" to workspace deps (+ card_game v0.3.0,
  arrayvec v0.7.6 as transitives via the Quaternions registry)
- Add klondike as a solitaire_core dep

Step 3 — KlondikeConfig / MoveFromFoundationConfig:
- KlondikeAdapter::new(draw_mode, take_from_foundation) builds a
  KlondikeConfig with the correct DrawStockConfig and
  MoveFromFoundationConfig (Allowed/Disallowed); exposes it via
  klondike_config() for future solver and pile-mapping steps

Step 4 — Scoring via ScoringConfig:
- GameState.adapter (serde(skip)) owns the authoritative KlondikeConfig
  with ScoringConfig::DEFAULT (WXP values)
- score_for_move/flip/undo/recycle replace direct scoring.rs calls;
  scoring.rs retained for reference and future deletion
- score_for_recycle implements the WXP free-recycle allowance rule
  that ScoringConfig::recycle cannot express (flat delta)
- PartialEq/Eq for KlondikeAdapter compare draw_stock and
  move_from_foundation only (scoring is always DEFAULT)

All 192 solitaire_core tests pass; clippy -D warnings clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:34:22 -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 38e4c0341e feat(engine): reactive render — animations drive RequestRedraw, focused_mode reactive on Android
All per-frame animation tick systems now write MessageWriter<RequestRedraw>
each frame they have active work, allowing WinitSettings focused_mode to
switch from Continuous to reactive_low_power(100 ms) on Android.

Systems updated:
- advance_card_animations (CardAnimationPlugin)
- advance_card_anims (AnimationPlugin — deal/win cascade)
- tick_shake_anim, tick_settle_anim, tick_foundation_flourish (FeedbackAnimPlugin)
- drive_toast_display (AnimationPlugin — toast countdown)
- drive_auto_complete (AutoCompletePlugin — step interval keepalive)

The 100 ms low-power ceiling means the game timer still ticks ~10×/s
with no input; animations self-sustain via the redraw chain at full
frame rate while active; and the GPU is completely idle between frames
when the board is static.

Each plugin registers add_message::<RequestRedraw>() so the message
type is available under MinimalPlugins in unit tests.

Closes #78, #79

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 13:54:54 -07:00
funman300 ccf280ea50 fix(engine): add missing modal scrim guard to leaderboard panel
Android Release / build-apk (push) Successful in 4m29s
toggle_leaderboard_screen was missing the other_modal_scrims guard that
all other panel-toggle systems have. Pressing L (or the HUD button) while
any other modal was open would spawn a second ModalScrim on top of the
existing one, breaking z-ordering and leaving the first modal un-dismissable.

Adds:
  other_modal_scrims: Query<(), (With<ModalScrim>, Without<LeaderboardScreen>)>
and the early-return guard before spawn_leaderboard_screen is called.

Closes #77

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v0.39.1
2026-05-28 15:52:47 -07:00
funman300 f1d96012f1 fix(engine): add modal scrim guard to toggle_stats_screen (#75)
Pressing S (or the Stats HUD button) while another modal was open
(Settings, Profile, Leaderboard, etc.) would spawn a second ModalScrim
on top of the existing one, violating the one-scrim-at-a-time invariant.

Add other_modal_scrims: Query<(), (With<ModalScrim>, Without<StatsScreen>)>
matching the guard pattern used by every other modal-spawning system.
Also import ModalScrim which was previously not imported in this file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 15:19:24 -07:00
funman300 7eb1181e50 fix(server): accept nil user_id placeholder in push; use received_at for leaderboard (#73, #74)
Build and Deploy / build-and-push (push) Successful in 3m37s
- sync.rs: replace Uuid::nil() placeholder with the authenticated
  user's real UUID before the mismatch check so desktop client pushes
  no longer fail with 400 user_id mismatch (#73)
- replays.rs: use server-computed received_at instead of client-supplied
  header.recorded_at when updating leaderboard recorded_at to prevent
  timestamp spoofing (#74)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 14:41:02 -07:00
funman300 f444378184 fix(engine): toast on challenge exhaustion, block input during auto-complete (#71, #72)
- challenge_plugin: replace silent warn+return with InfoToast when all
  challenges are completed so the player gets clear feedback (#72)
- input_plugin: add AutoCompleteState guard to start_drag,
  touch_start_drag, and handle_double_tap so player input cannot race
  with the auto-complete move sequence (#71)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 14:24:09 -07:00
funman300 927598202e feat(engine,data): add tap-to-select touch input mode (#70)
- Add TouchInputMode enum (OneTap | TapToSelect) to solitaire_data settings
- Create TouchSelectionPlugin with TouchSelectionState resource and highlight
- Branch handle_double_tap: OneTap → existing auto-move, TapToSelect → two-tap flow
- Add Settings UI toggle row (Touch Input Mode) with TouchInputModeText marker
- Register TouchSelectionPlugin in CoreGamePlugin

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 14:04:40 -07:00
funman300 6e407a3ea7 fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
Build and Deploy / build-and-push (push) Successful in 3m54s
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 13:07:22 -07:00
funman300 8cb4c9808e fix(wasm,stats): surface replay errors to JS, deduplicate win events per frame (#65, #69)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 21:53:15 -07:00
funman300 dbe728fef7 refactor(engine): deduplicate TABLEAU_FAN_FRAC constant (#59)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 19:57:49 -07:00
funman300 0437c36463 fix(assets,theme): remove assert in svg_loader, log theme failures, fix default theme id (#58, #63, #64)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 19:36:05 -07:00
funman300 35fde160fa fix(ui): add modal guard to profile, make modal dimensions responsive (#57, #67)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 19:21:09 -07:00
funman300 cfdf27c8c7 fix(time-attack): clamp timer to zero and pause during overlays (#54, #55)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 19:13:42 -07:00
funman300 bd49364553 fix(android): replace forbidden Unicode chars in win_summary and splash (#52, #53)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 19:03:11 -07:00
funman300 a3b9293cd9 chore(engine): final cleanup after platform abstraction refactor (closes #51)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 18:08:00 -07:00
funman300 ce536b0176 refactor(engine): audit and rationalize platform cfg gates (closes #49)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 18:00:57 -07:00
funman300 561395fca6 feat(data,engine): implement NativeStorage and WasmStorage backends (closes #48)
Build and Deploy / build-and-push (push) Successful in 3m59s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 17:30:35 -07:00
funman300 a8ceed97a9 refactor(engine): migrate gameplay plugins into CoreGamePlugin (closes #45, closes #46)
All engine plugin registrations now live in CoreGamePlugin::build().
build_app() is reduced to DefaultPlugins setup + CoreGamePlugin registration.
sync_provider is threaded through CoreGamePlugin::new() via Mutex<Option<...>>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 17:08:54 -07:00
funman300 86bafdd679 feat(engine): add platform abstraction trait skeleton (closes #47)
Adds solitaire_engine::platform::{StorageBackend, PlatformTime} traits.
No implementations yet — native and WASM impls follow in #48.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 16:58:42 -07:00
funman300 3885b334ec refactor(app): extract build_app(), add CoreGamePlugin placeholder (closes #42, closes #44)
- Split run() into build_app(sync_provider) -> App and run()
- Add empty CoreGamePlugin registered in build_app()
- Issue #43 closed via API (main.rs already satisfies it)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 16:41:40 -07:00
funman300 5a71e2bc0a fix(engine): ensure dragged card stack z-order is above all piles (closes #35)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 16:29:01 -07:00
funman300 04aea8595a docs(claude): add dealsbe.com AI tools directory to user resources
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 15:13:06 -07:00
funman300 25c43db61e fix(ci): use git switch to avoid deploy dir/branch ambiguity
Build and Deploy / build-and-push (push) Successful in 20s
'git checkout deploy' is ambiguous because the repo contains both a
deploy/ directory and a deploy remote tracking branch. Switch to
'git switch' which is branch-only and unambiguous.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:44:35 -07:00
funman300 c2eff2ed96 ci: add comment to retrigger docker build
Build and Deploy / build-and-push (push) Failing after 21s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-27 14:38:37 -07:00