Commit Graph

20 Commits

Author SHA1 Message Date
funman300 4303ef3f5b feat(difficulty): add difficulty-tier game mode with seed catalogs and home UI
Adds DifficultyLevel (Easy/Medium/Hard/Expert/Grandmaster/Random) to
solitaire_core::game_state alongside GameMode::Difficulty(DifficultyLevel).
Five seed catalogs (40 seeds each) are pre-verified by the new
gen_difficulty_seeds binary using tiered solver budgets (1K–200K moves).
DifficultyPlugin resolves StartDifficultyRequestEvent → catalog seed →
NewGameRequestEvent; Random uses a system-time seed and bypasses the
winnable-only filter. The home overlay gets an expandable Difficulty section
between Draw Mode and the mode grid; last-played tier persists in Settings.
Difficulty wins pool into Classic stats. 5 unit tests in difficulty_plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:07:49 -07:00
funman300 0cb15872b1 feat(engine): add Play-by-Seed dialog with solver preview
Adds a numeric-input modal (PlayBySeedPlugin) that lets the player type
a decimal seed and receive an instant solver-verified verdict before the
hand is dealt.  A new HomeMode::PlayBySeed card surfaces it in the home
overlay, matched by the StartPlayBySeedRequestEvent carrier.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:19:02 -07:00
funman300 ec804d54c6 feat(accessibility): finish HC chrome rollout — home + settings panel borders
Continues the rollout from `c9af1ea` (modal scaffold) and
`d87761d` (tooltip + 3 panels). Tags the remaining 7 static-
border surfaces in the chrome so the HC chrome thread is
effectively complete:

- **`home_plugin.rs` × 3**: the home-screen Level/XP/Score
  summary row (line 842), the home-screen mode-selector
  buttons (line 945), the home-screen mode-hotkey chips
  (line 1158).
- **`settings_plugin.rs` × 4**: the card-back picker swatches
  (line 1952), the theme picker swatches (line 2093), the
  Sync Now button (line 2214), and the swatch glyph buttons
  (line 2274).

Pre-tagging audit: confirmed none of these sites have a
dynamic-paint system that would race the
`update_high_contrast_borders` system. `paint_action_buttons`
in `hud_plugin.rs` only paints entities tagged with the
`ActionButton` marker (HUD buttons only). The focus-overlay
system in `ui_focus.rs` spawns *separate* overlay entities for
focus indication, never mutating the original `BorderColor`.
Settings panel buttons / swatches use their own
`SettingsButton` enum for click routing; their `BorderColor`
is set at spawn time and not touched again.

After this commit, every `BorderColor::all(BORDER_SUBTLE)` site
in the chrome (excluding the dynamic-paint sites that are
intentionally skipped — HUD action buttons, modal buttons,
radial menu rim) carries a `HighContrastBorder` marker. The
HC thread for chrome borders is closed; the dynamic-paint
sites remain open for a future iteration that needs a
different shape (folding HC into the dynamic-paint logic, or
having HC consult hover/focus state).

1194 passing / 0 failing across the workspace (unchanged — no
new tests; the system-level lifecycle of `HighContrastBorder`
was already covered by the modal-scaffold scaffolding in
`c9af1ea`). Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:47:58 -07:00
funman300 9aa0dd23b1 fix(engine): Esc dismisses the topmost modal when Profile stacks on Home
Clicking the new Home header chip opens Profile on top of Home.
Pressing Esc then closed Home (because handle_home_cancel_button
fired on Esc with no awareness of layered modals) and left Profile
orphaned over the game — the player had to press P afterwards just
to dismiss what they meant to dismiss in the first place.

Two changes restore the standard "Esc closes the topmost modal"
contract:

- profile_plugin: split P/button (toggle) from Esc (close-only).
  Esc only fires when Profile is currently open.
- home_plugin: handle_home_cancel_button now skips its Esc branch
  when any other ModalScrim exists, deferring to whichever modal
  is on top. Click on the explicit Cancel button is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:15:18 +00:00
funman300 d065d49fe7 fix(engine): TimeAttack tile glyph swaps to → (FiraMono ships sideways
triangles inconsistently)

Quat: ▶ (U+25B6) rendered as tofu even though ▲ (U+25B2) from the
same Geometric Shapes block works. FiraMono evidently ships the
up/down triangles but not the left/right siblings.

Swapped to U+2192 (RIGHTWARDS ARROW) from the Arrows block, which
is part of every dev-oriented monospace font's core coverage. Reads
as "go / fast-forward" for the timed mode and is visually distinct
from the other 4 tile glyphs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:06:40 +00:00
funman300 c30b04ec72 fix(engine): Home tile glyphs picked from FiraMono's actual coverage
The bundled face is FiraMono-Medium (assets/fonts/main.ttf), and its
glyph table covers card suits (U+2660-2666) plus basic Geometric
Shapes (U+25xx) but not Dingbats / Misc Symbols. The previous round
of "BMP fallbacks" still picked from blocks FiraMono doesn't cover,
so 4 of 5 tiles continued to render as tofu.

Re-picked from ranges FiraMono actually has:
- Daily: U+25C6 (BLACK DIAMOND)
- Zen:   U+25CB (WHITE CIRCLE) — Zen enso
- Challenge: U+25B2 (BLACK UP-POINTING TRIANGLE) — climbing
- TimeAttack: U+25B6 (BLACK RIGHT-POINTING TRIANGLE) — play / FF
- Classic keeps U+2663 (BLACK CLUB SUIT)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:00:26 +00:00
funman300 40d6e0ab17 fix(engine): Home tile glyphs render + modal fits any viewport
Two regressions Quat caught in screenshot review of the picture-tile
rework:

1. Tofu boxes for 4 of 5 tiles. The earlier emoji picks (calendar,
   cherry-blossom, lightning, stopwatch) live in Unicode planes that
   most Linux desktop fonts don't cover, so they rendered as
   missing-glyph rectangles. Swapped to BMP / Dingbats codepoints
   that the system-default font fallback always has:
   - Daily: \u{2605} (BLACK STAR)
   - Zen:   \u{2740} (WHITE FLORETTE)
   - Challenge: \u{2726} (BLACK FOUR-POINTED STAR)
   - TimeAttack: \u{231A} (WATCH, Misc Symbols / Unicode 1.1)
   Classic keeps its club (\u{2663}) — already rendered correctly.

2. Cancel button pushed off the bottom of the viewport. The 3-row
   tile grid alone is ~540 px; on the 800x600 minimum window the
   modal exceeded the screen. Wrapped chips + draw row + grid in a
   `HomeScrollable` Node with `max_height: 70vh` and `Overflow::scroll_y()`,
   adding a `scroll_home_panel` system to drive `ScrollPosition` from
   `MouseWheel`. Mirrors the existing Settings / Leaderboard /
   Achievements scrollable pattern. Cancel sits outside the scroll
   so it's always reachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:52:44 +00:00
funman300 9fe650fa20 feat(engine): Home picker — 2-column picture tiles with Unicode glyphs
Phase B step 2 of the MSSC-inspired Home rework. Mode cards become a
wrapping 2-up grid with a centred Unicode-glyph centrepiece per tile,
standing in for real per-mode artwork until that lands.

- HomeMode::glyph() returns the placeholder codepoint for each mode:
  ♣ Classic, calendar Daily, cherry-blossom Zen, lightning Challenge,
  stopwatch TimeAttack. Cherry-blossom is used over lotus-position
  because the latter renders inconsistently across desktop fonts.
- The mode-card loop is wrapped in a FlexWrap::Wrap row container.
  Tiles set `width: 48%` + `min_height: 180px`; the 5-mode grid
  wraps to a third row of one tile, mirroring the half-cell asymmetry
  in MSSC's screenshot.
- The glyph paints in ACCENT_PRIMARY when the mode is unlocked and
  TEXT_DISABLED when locked, so the gate reads at a glance.
- When real art lands, swap the Text node for an Image node — the
  rest of the tile layout, focus order, click handling, and chip
  rendering are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:45:30 +00:00
funman300 b73d246b4c feat(engine): Today's Event callout on the Home Daily card
Phase B step 1 of the MSSC-inspired Home rework — surfaces today's
daily-challenge metadata on the Daily card so the picker reads as
"there's something fresh waiting" rather than a generic mode label.

- Date line "Today, May 6" pulled from DailyChallengeResource. Reads
  in STATE_INFO blue while the run is still open.
- Server-fetched goal (when SyncPlugin is wired) appears underneath
  as "Goal: Win in under 5 minutes", matching the toast that already
  fires when the player presses C.
- Once the player has recorded today's completion, the date flips
  to "Today, May 6 \u{2022} Done" in ACCENT_PRIMARY so the picker
  reads as a reward state rather than a TODO.

Headless tests omit DailyChallengePlugin, so HomeContext.daily_today
defaults to None and the card falls back to its baseline layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:28:59 +00:00
funman300 ae40a1db7a feat(engine): MSSC-style Home picker — header chips, score chips, draw mode
Phase A of the Microsoft-Solitaire-Collection-inspired launch picker
rework. Three additive changes inside the Home modal, no core / asset
work:

- Player-stats header strip showing Level / XP / Lifetime Score using
  a compact formatter (1.2M / 12.3K / 1,234). The whole strip is a
  Button — click fires ToggleProfileRequestEvent so Profile opens on
  top of Home; closing it returns to the picker.
- Draw-mode chip row above the mode cards lets the player flip
  Draw 1 / Draw 3 from the picker itself rather than diving into
  Settings. Active chip uses ACCENT_PRIMARY background; the click
  persists settings.json and respawns the modal so the active state
  repaints cleanly.
- Per-mode score/streak chip on each card — "Best 12,345" for
  Classic / Zen / Challenge, "Streak N" for Daily. Hidden on a 0
  best so a fresh profile doesn't read "Best 0" everywhere.

`HomeContext` bundle pulls live data from ProgressResource /
StatsResource / SettingsResource with safe defaults so headless
tests under MinimalPlugins still build cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:16:01 +00:00
funman300 b7c3a4996f fix(engine): Restore-prompt resolution suppresses Home auto-show
Resolving the Welcome-back / Restore prompt (either Continue or New
game) cleared `PendingRestoredGame` and despawned the modal, but the
launch-time Home auto-show then fired the next frame and stacked
itself over the player's chosen path — clicking "New game" would deal
a fresh game AND immediately pop the mode picker on top.

`LaunchHomeShown` becomes pub so `handle_restore_prompt` can flip it
to `true` after either resolution; `M` still re-opens the picker on
demand. Headless tests already pre-set the flag to true via
`HomePlugin::headless()`, so they're unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:44:31 +00:00
funman300 d48b9489db feat(engine): Esc dismisses Home / accepts default on Restore prompt
Home and Restore-prompt previously ignored Esc, which after the last
fix meant Esc just did nothing on those screens. Now both honor the
"Esc closes the modal" convention every other modal already follows.

- Home: Esc behaves like the Cancel button — despawns the modal so
  the player keeps the underlying default deal.
- Restore: Esc maps to Continue rather than New Game; a reflexive
  dismiss press preserves the saved game, matching how the primary
  action already advertises the Enter accelerator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:36:09 +00:00
funman300 dd63261999 feat(engine): auto-show Home / mode picker on launch
The Home (mode picker) was only reachable via M during gameplay, so
players who hadn't discovered the hotkey never saw the Daily / Zen /
Challenge / Time Attack entry points after the splash cleared.

- HomePlugin gains an `auto_show_on_launch` flag (default true) and a
  matching `headless()` test constructor that disables it.
- spawn_home_on_launch flips a one-shot LaunchHomeShown flag once the
  splash has cleared, gated on RestorePromptScreen / PendingRestoredGame
  so the Welcome-back flow still takes precedence on machines with a
  saved game.
- App entry uses HomePlugin::default(); both headless test fixtures
  switch to HomePlugin::headless() so per-test worlds start clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 06:57:25 +00:00
funman300 cbf2483028 feat(engine): opt Profile / Leaderboard / Home into scrim-click dismiss
Follow-up to a54201e. The previous commit added ScrimDismissible to
Stats, Achievements, and Help; this one extends the same one-line
opt-in to the remaining three read-only modals so the click-outside-
to-close gesture is consistent across every informational surface.

Each modal now has the same shape: capture the scrim from
spawn_modal, attach ScrimDismissible after the build closure
returns. Three lines per file plus the import; no behaviour change
to the modal content itself.

Settings, Onboarding, Pause, Forfeit confirm, ConfirmNewGame, and
the win/game-over modals continue to opt OUT — all carry unsaved
or destructive state where an accidental scrim click would lose
work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:47:02 +00:00
funman300 7dba772e67 feat(engine): digit shortcuts (1-5) launch modes from inside the Mode Launcher
Pressing M already opens the Home modal (which is the Mode Launcher
post-v0.11) and Tab cycles focus through the cards. The remaining
gap was direct keyboard activation of a specific mode — players had
to tab-and-enter or click. A new modal-scoped digit handler closes
that gap:

  1 → Classic (NewGameRequestEvent)
  2 → Daily Challenge (StartDailyChallengeRequestEvent)
  3 → Zen (StartZenRequestEvent, gated at level 5)
  4 → Challenge (StartChallengeRequestEvent, gated at level 5)
  5 → Time Attack (StartTimeAttackRequestEvent, gated at level 5)

handle_home_digit_keys runs only when HomeScreen exists and short-
circuits otherwise — the digit keys can't accidentally launch a
mode mid-game. Locked modes (level < CHALLENGE_UNLOCK_LEVEL) silent-
no-op rather than firing a toast, mirroring the click-on-locked-card
behaviour without the InfoToastEvent (the click path's toast is the
authoritative "level too low" surface).

The HomePlugin Update tuple is now .chain()ed because the Bevy 0.18
parallel scheduler would otherwise let handle_home_card_click,
handle_home_cancel_button, and the new digit handler all queue a
HomeScreen despawn concurrently — the second buffer apply panics
on the already-despawned entity.

help_plugin gains a new "Mode Launcher (M)" section with the digit
rows and a level-5 unlock note. onboarding's slide-3 hotkey table
gets one new line ("M — Open Mode Launcher (then 1-5 to pick)") so
first-run players see the full path. The help-modal canonical list
now mirrors the onboarding teach.

Four new headless tests pin the contract: Digit1 launches Classic
and closes the modal; Digit3 at level 0 is a no-op (modal stays
open); Digit3 at unlock level launches Zen and closes; digit keys
outside the modal fire no events at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 03:01:41 +00:00
funman300 51d3454344 feat(engine): keyboard focus on HUD action bar and Home mode cards (Phase 2)
The HUD action bar (Menu / Undo / Pause / Help / Modes / New Game) and
the five Home mode-launcher cards now participate in keyboard focus,
extending Phase 1's modal-only coverage.

The HUD focus group activates only when no modal is open and the
mouse is hovering an action-bar button — the design decision avoids
stealing Tab from selection_plugin's card-selection nav for the
common "playing on the board" case. Once engaged, Tab/Shift-Tab cycles
the bar in spawn order and Enter activates. Moving the mouse off the
bar clears focus so the ring doesn't linger.

Home mode cards opt into FocusGroup::Modal(home_scrim) via an
ancestry-walking system that mirrors the Phase 1 attach helper, so
spawn_mode_card's signature is unchanged. Locked cards (Zen,
Challenge, Time Attack at level <5) get the Disabled marker so Tab
skips them and Enter is a no-op — mirroring the existing visual
locked state with real keyboard semantics.

handle_focus_keys gains a Hud-on-hover branch in its active-group
resolver and a clear_hud_focus_on_unhover system. Together they
implement the agreed UX: focus follows hover when the bar is active,
Tab cycles within the hovered group, and the ring disappears the
instant the mouse leaves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:41:31 +00:00
funman300 c1bde18a2c feat(engine): repurpose Home as mode launcher
The Home modal was previously a keyboard-shortcut reference card that
mostly duplicated Help. It now opens directly into a Mode Launcher:
five mode cards (Classic, Daily Challenge, Zen, Challenge, Time
Attack) stacked vertically with a Cancel button at the bottom.

Each card dispatches the canonical request event already used by the
HUD modes-popover (NewGameRequestEvent, StartDailyChallengeRequestEvent,
StartZenRequestEvent, StartChallengeRequestEvent,
StartTimeAttackRequestEvent), so level gates, daily-seed lookup, and
session setup all flow through the existing handlers — Home is just
another entry point.

The three modes that unlock at level 5 (Zen, Challenge, Time Attack)
render with reduced opacity and a "Reach level 5 to unlock" caption
when locked; clicking a locked card is a deliberate no-op so the
player can pick a different mode without dismissing the modal.

The keyboard-shortcut reference is dropped entirely — Help (F1) still
covers it. M continues to toggle the modal open and closed.

Adds 5 new headless tests covering card spawn, locked-state click,
unlocked-state click, Classic launch + close, and Cancel close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:39:26 +00:00
funman300 3b619b8950 feat(engine): convert HomeScreen to modal scaffold + Done button
Phase 3 step 5f of the UX overhaul. Closes the per-overlay
conversion phase: every read-only overlay (Help, Stats, Achievements,
Profile, Leaderboard, and now Home) sits inside the same ui_modal
scaffold, picks colours from ui_theme, and dismisses via a real
"Done" primary button alongside its keyboard accelerator.

Home modal:
- Header: "Solitaire Quest"
- Mode badge: "Current mode: <mode>" in ACCENT_PRIMARY (yellow)
- Two sections (Game Controls / Screens), each rendering keyboard
  shortcuts as kbd-chip rows — the same pattern Help uses, so the
  two reference screens read consistently. Section titles use
  STATE_INFO.
- "L" leaderboard row added so the screens list is now complete.
- Actions: primary Done button with the M hotkey chip.
- handle_home_close_button is the click counterpart to M.

Home overlap with Help is intentional during the overhaul — both
exist as hotkey references for now. A future commit can repurpose
Home as a true mode launcher (the proposal called for this) or
remove it entirely if Help is sufficient. Either path is easier with
both screens already in the consistent shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:44:33 +00:00
funman300 c8553dc8c5 chore(deps): migrate to Bevy 0.16, axum 0.8, and other package updates
- Bump bevy 0.15 → 0.16; fixes all breaking API changes:
  ChildBuilder → ChildSpawnerCommands, Parent → ChildOf,
  despawn_descendants → despawn_related::<Children>(),
  despawn_recursive → despawn (now recursive by default),
  EventWriter::send → write, Query::{get_single,get_single_mut}
  → {single,single_mut}, ChildOf::get → parent()
- Bump axum 0.7 → 0.8; remove axum::async_trait from FromRequestParts
- Bump tower_governor 0.4 → 0.8; fix GovernorLayer::new() API
- Bump jsonwebtoken 9 → 10 with rust_crypto feature only
- Bump thiserror 1 → 2, dirs 5 → 6, bcrypt 0.15 → 0.19,
  reqwest 0.12 → 0.13 (rustls feature rename)
- Regenerate .sqlx offline cache for sqlx compile-time query checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:31:12 -07:00
funman300 03227f8c77 feat(engine): playability improvements — rounds 7–9 (#40–#64)
Round 7 — Input & feedback
- H key cycles hints; F1 opens help (conflict resolved)
- N key cancels active Time Attack session
- Hint text distinguishes "draw from stock" vs "recycle waste"
- Forfeit (G) clears AutoCompleteState so chime does not bleed into new deal
- Zen mode timer clears immediately on Z press
- HUD shows recycle count in both draw modes
- Settings scroll position persisted across open/close

Round 8 — Polish & clarity
- Undo unavailable fires "Nothing to undo" toast
- Streak-break toast on forfeit/abandon when streak > 1
- F11 fullscreen toggle with toast; documented in help and home screens
- H-after-win, new-game countdown expiry, Tab-no-cards toasts
- Win cascade duration/stagger scales with animation speed setting
- Draw-Three cycle counter HUD ("Cycle: N/3")
- Forfeit requires G×2 confirmation within 3 s (mirrors N key)

Round 9 — Game feel & information
- Escape dismisses game-over/stuck overlay (PausePlugin skips Escape when overlay visible)
- Shake animation on rejected drag before snap-back
- Forfeit countdown cancels when any other key is pressed (U/H/D/Z/Space)
- Tab wrap-around fires "Back to first card" toast
- HUD selection indicator shows active Tab-selected pile in yellow
- Challenge time-limit HUD turns orange < 60s, red < 30s
- Win summary shows XP breakdown (+50 base, +25 no-undo, +N speed)
- Game-over overlay: "No more moves available" with clear N/Escape/G instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 02:35:15 +00:00