Verifies that pressing X when the player's level is below
CHALLENGE_UNLOCK_LEVEL emits an InfoToastEvent containing the unlock
level, rather than silently doing nothing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two tests in progress_plugin: verify XpAwardedEvent fires with the
correct amount on a slow no-undo win (75 XP), and verify LevelUpEvent's
total_xp field matches the ProgressResource after the win.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(input_plugin): card_position() now applies the same X-fan offset for
Draw-Three waste cards as card_plugin uses for rendering. Previously the
top waste card appeared at base_x + 0.56 * card_width but was only
hittable at base_x, making it impossible to drag from its visual position.
test(hud_plugin): add five behaviour tests — score/moves/time display
format, Zen mode score suppression, Draw-Three mode badge.
test(input_plugin): add find_draggable test that clicks the top fanned
waste card at its visual X position and confirms it hits in Draw-Three.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only the top N waste cards are now passed through card_positions():
Draw-One renders 1 card, Draw-Three renders up to 3 fanned in X
(standard Klondike layout). Non-top waste card entities are despawned,
so nothing is visible at the waste position while the newly drawn card
slides in from the stock — the bleed-through bug is gone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
leaderboard_plugin: format_secs was missing the 60-second crossing
boundary (1:00 vs 60s), the zero case (0s), and leading-zero padding
verification (1:05 not 1:5).
pause_plugin: added third-press test confirming the Esc toggle is
symmetric across multiple pause/resume cycles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The advance_time_attack system has an early-return path when
PausedResource is true, but this branch had no test coverage.
New test: with remaining_secs = -1 (normally triggers expiry),
inserting PausedResource(true) must suppress the ended event
and leave remaining_secs negative (timer did not advance).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Reward::BonusXp path in evaluate_on_win was adding XP directly
without sending XpAwardedEvent, so players saw no "+25 XP" toast when
the no_undo achievement first unlocked. Adds the missing event send
and a regression test verifying the event fires on unlock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Daily challenge completions (+100 XP) and weekly goal bonuses (+75 XP)
now fire XpAwardedEvent so the player sees a "+N XP" toast — consistent
with the post-win XP toast already shown by ProgressPlugin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously every SettingsChangedEvent fired an "SFX: X%" toast, even
when the music volume was adjusted in the Settings panel. Now the toast
handler tracks the previous SFX and music levels independently and only
shows a toast for whichever value actually changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Face-down cards in tableau columns now use a TABLEAU_FACEDOWN_FAN_FRAC
of 0.12 instead of the full 0.25, so the visible face-up portion of
each column is easier to read at a glance — matching standard Klondike
rendering where only the playable cards are prominently spread.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two new weekly goals — "Win 1 game in under 5 minutes" and
"Win 1 Draw-3 game" — broadening variety beyond the existing three.
WeeklyGoalContext gains a draw_mode field so the new WinDrawThree
variant can match on draw mode. Existing tests updated to pre-complete
new goals where the win conditions overlap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zen mode is intended for relaxed play with no performance pressure.
The HUD now clears score and elapsed-time displays when GameMode::Zen
is active, matching the ARCHITECTURE.md spec ("No timer. No score
display."). The mode badge still shows "ZEN" so the player knows
which mode they're in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProgressPlugin now fires XpAwardedEvent on every win. AnimationPlugin
shows a "+N XP" toast so players see XP feedback immediately after
winning. Server leaderboard opt-in endpoint also now validates that
display_name is at most 32 characters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces silent info!() log calls with on-screen toasts when the player
presses Z/X/T without reaching the required unlock level. Any system
can now fire InfoToastEvent(message) to surface a brief text overlay
without depending on a specific plugin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing N during an active game (move_count > 0, not won) now shows a
"Press N again to start a new game" toast and only starts a new game if
N is pressed a second time within 3 seconds. Starting a fresh game or
pressing N after a win still acts immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a move exposes a face-down tableau card, game_plugin now fires
CardFlippedEvent carrying the flipped card's id. AudioPlugin listens
and plays card_flip.wav so the reveal has satisfying audio feedback.
Two unit tests verify the event fires only when needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each achievement row now displays:
- "Reward: Card Back #N / Background #N / +N XP / Badge" (green, all entries)
- "Unlocked YYYY-MM-DD" (dim, unlocked entries only)
Helps players understand what they earn and when they earned it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Classic + DrawOne shows nothing (clean default). Classic + DrawThree shows
a "Draw 3" badge in the mode position so the player always knows their
current game variant without opening the stats or settings screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds "Next Level: N XP (P%)" line so players can see how far they are
through the current level without doing the arithmetic themselves.
Tested with three unit tests covering level 0, mid-level, and level 10.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
During Time Attack sessions the time display updates every frame with the
remaining countdown (from TimeAttackResource) rather than the per-second
game clock tick, giving the player a live countdown. Non-Time-Attack mode
is unchanged — the clock still updates once per second via game state change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"You Win!" now displays "You Win! Score: N Time: M:SS" so the player
sees their result without opening the stats screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inner card capped at 88% of window height with Overflow::clip_y so
the panel stays on-screen even when many rows are present. Matches the
same approach used by the leaderboard panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stats screen (S key) now shows "Challenge: N / total" in the Progression
section so players can see how far they've advanced through the challenge
seed list without leaving the screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds HudPlugin with a persistent top-left overlay that shows score,
move count, and elapsed time during every game. A mode badge highlights
DAILY, CHALLENGE, ZEN, or TIME ATTACK when the game is not in Classic
mode. HUD updates whenever GameStateResource changes (moves and per-second
time ticks) without a separate polling system.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Card backs: selected_card_back index maps to distinct Color values in card rendering
- Backgrounds: selected_background index applied in TablePlugin alongside theme
- Both re-render immediately on SettingsChangedEvent
- Stats screen now shows Games Lost, Draw 1/3 Wins, and Lifetime Score
- Daily challenge win no longer credited if server-supplied target_score or max_time_secs constraints are not met
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
evaluate_weekly_goals discarded the return value of add_xp(bonus_xp),
so a level-up triggered by a weekly goal completion never fired
LevelUpEvent — the level-up toast and mode-unlock at L5 were silently
skipped. Now captures prev_level, checks leveled_up_from(), and sends
LevelUpEvent matching the pattern used by progress_plugin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fetch_daily_challenge() to SyncProvider trait (default: Ok(None))
- SolitaireServerClient calls GET /api/daily-challenge (public endpoint)
and returns the ChallengeGoal; non-2xx responses return Ok(None) so
callers fall back to the local date-hash seed
- DailyChallengePlugin spawns an async task on Startup (only when
SyncProviderResource is present) and polls it in Update; on success
it overwrites DailyChallengeResource.seed with the server's seed,
ensuring all players worldwide get the same deal on a given date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PullTask and PullTaskResult now carry Result<SyncPayload, SyncError>
instead of Result<SyncPayload, String>. poll_pull_result pattern-matches
on the error variant to show user-friendly messages:
Network → "Can't reach server — check your connection"
Auth → "Login expired — tap Sync Now after re-logging in"
Other → original error Display
Also removed the stale TODO comment from SyncError in lib.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
evaluate_weekly_goals only ran the roll on GameWonEvent, so stale goal
progress from a prior week would linger until the player's next win.
A new Startup system calls roll_weekly_goals_if_new_week on launch and
persists immediately when the week has rolled over.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Reward enum to solitaire_core with CardBack/Background/BonusXp/Badge variants
- Wire rewards into ALL_ACHIEVEMENTS per architecture spec
- evaluate_on_win now applies rewards on first unlock: pushes cosmetic
indices into PlayerProgress, awards BonusXp (with level-up detection),
and marks reward_granted = true so rewards are never double-granted
- Add selected_card_back / selected_background fields to Settings
- Settings panel grows Card Back and Background cycle rows, shown only
when the player has unlocked more than the default (index 0)
- cycle_unlocked() cycles only through earned options
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The help overlay only listed S and H; added Achievements (A),
Leaderboard (L), and Settings (O) which were already implemented
but undocumented in the cheat sheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Press L to open a leaderboard overlay. On open, an async fetch is
dispatched against the active SyncProvider. Results cache in
LeaderboardResource; the panel rebuilds live when data arrives.
Closing during a fetch is safe (ClosedThisFrame flag prevents
re-spawning the panel in the same frame as the user's despawn command).
Format: ranked table with player name, best score, and fastest win time.
Non-authenticated / LocalOnly providers return an empty list gracefully.
- solitaire_data: add fetch_leaderboard() to SyncProvider trait
(default → Ok([])) and implement in SolitaireServerClient
- solitaire_engine: new LeaderboardPlugin with 5 unit tests
- solitaire_app: register LeaderboardPlugin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a full-screen overlay listing all achievements with unlock status.
Unlocked achievements show in gold with a check mark; locked ones are
greyed out. Secret achievements that are still locked are hidden. Header
shows unlocked/total count. Press A again to dismiss.
Two new unit tests: spawns on first A press, dismisses on second.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When is_auto_completable flips true (stock/waste empty, all cards
face-up), AutoCompletePlugin fires MoveRequestEvent every 120 ms,
driving cards to the foundation one at a time without player input.
An "Auto-completing…" toast announces the sequence.
- solitaire_core: add next_auto_complete_move() to GameState with
3 new unit tests
- solitaire_engine: new AutoCompletePlugin with detect + drive systems
and 4 unit tests; animation_plugin shows one-shot toast on activation
- solitaire_app: register AutoCompletePlugin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save game_state.json on app exit and on pause open so players can
resume interrupted sessions. Delete the file on win, loss, or new-game
start. Restore the saved game on launch if it exists and isn't won.
- solitaire_core: add pile_map_serde module so HashMap<PileType,Pile>
round-trips through JSON (serialized as Vec of pairs)
- solitaire_data: add game_state_file_path, load_game_state_from,
save_game_state_to, delete_game_state_at with 8 new unit tests
- solitaire_engine/GamePlugin: restore saved game on startup, expose
GameStatePath resource, save on AppExit, delete on new-game and win
- solitaire_engine/PausePlugin: save on pause open (guards against
OS-level kills while the overlay is showing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added ManualSyncRequestEvent to events.rs (exported from lib.rs).
- SyncPlugin now handles ManualSyncRequestEvent: if no pull is in
flight, spawns a new AsyncComputeTaskPool task and sets status to
Syncing. Ignores duplicate requests while a pull is active.
- Settings panel "Sync" section now shows the status text alongside a
"Sync Now" button that fires ManualSyncRequestEvent.
- Cleaned up stale doc comment in input_plugin.rs (Esc pause note).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Settings panel now shows a "Sync" section with a live status line:
- "Status: idle" (no SyncPlugin installed)
- "Status: syncing…" (pull in progress)
- "Last synced: Xs ago" (successful pull)
- "Sync error: <msg>" (failed pull)
The text is snapshotted from SyncStatusResource when the panel opens and
updated reactively via update_sync_status_text whenever SyncStatusResource
changes while the panel is visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings panel "coming soon" stubs replaced with live controls:
- Draw Mode toggle (Draw 1 / Draw 3): new games read draw_mode from
SettingsResource instead of the previous game's mode. Falls back to
the current game's mode in headless/test contexts where SettingsPlugin
is absent.
- Theme selector (Green → Blue → Dark → Green): SettingsChangedEvent
drives TablePlugin's background Sprite colour so the table re-colours
immediately without a restart.
- Music Volume [−]/[+]: dedicated kira sub-tracks created for SFX and
music on startup. SFX sounds are routed to the SFX track; the music
track exists for future ambient audio. Both volumes are set on
SettingsChangedEvent and at startup.
Also fixed: time_attack timer_expiry test double-fires when
MinimalPlugins time delta is nonzero — removed the intermediate
0.001-remaining update step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
challenge, account deletion, JWT middleware, rate limiting via
tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
SolitaireServerClient, auth_tokens keyring integration, blanket
Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
connections share the same schema
All 191 tests pass; cargo clippy -D warnings clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
Phase 6 part 4b (partial):
- GameMode::Challenge variant in solitaire_core. undo() returns
RuleViolation when mode is Challenge so the player commits to each
decision.
- solitaire_data::challenge defines a stable CHALLENGE_SEEDS list with
challenge_seed_for(index) wrapping modulo length.
- PlayerProgress.challenge_index (serde-default for older saves) tracks
how far the player has progressed.
- ChallengePlugin advances the cursor on Challenge-mode wins, persists,
and emits ChallengeAdvancedEvent. Pressing X starts a Challenge-mode
game with the current seed; gated to level >= CHALLENGE_UNLOCK_LEVEL (5).
- InputPlugin's Z key (Zen mode) is now also gated to level >= 5.
Time Attack and unlock UI still deferred.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>