The daily-challenge seed function had no unit test coverage. Added tests
for determinism, cross-day and cross-year distinctness, non-zero output,
and all six generate_goal variants (score/time field correctness).
This acts as a change-detection guard for the authoritative seed algorithm
that all players worldwide rely on to receive the same daily deal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seven stats fields (games_lost, win_streak_best, lifetime_score,
draw_one_wins, draw_three_wins, avg_time_seconds on both branches)
had no isolated test coverage in the merge test suite. Added boundary
tests for each, including a concrete arithmetic verification of the
weighted-average recomputation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both public APIs in solitaire_sync had no test coverage:
- win_rate(): None before any game, 100/50/0% cases
- AchievementRecord::locked(), unlock(), idempotency preserving earliest date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The leaderboard opt-in handler was calling `.len()` on the display name,
which returns byte count. Multi-byte Unicode characters (emoji, CJK, etc.)
would be rejected well before the 32-character visual limit and with a
misleading error message. Switched to `.chars().count()` to enforce the
limit in terms of Unicode scalar values as the error message advertises.
test(core): add boundary tests for 7 uncovered achievement conditions
test(server): add display_name validation integration tests (empty,
too-long ASCII, 32-emoji succeeds, 33-emoji rejected)
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>
Cover four previously untested rule branches:
- Moving a face-down card in a multi-card lift → RuleViolation
- Moving two cards to foundation → RuleViolation
- Requesting more cards than the pile holds → RuleViolation
- Valid 3-card sequence tableau→tableau → succeeds and updates move_count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, same-week progress from the remote device was discarded
entirely — local counts were always preferred. Now each goal's count
is merged with max() so progress earned on any device is preserved.
Adds two regression tests covering same-week and newer-week cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both predicates previously matched the same window (h < 6), making them
indistinguishable. night_owl now triggers 22:00–02:59 (late night) and
early_bird triggers 05:00–06:59 (pre-dawn). Updated descriptions and
tests to match the distinct windows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
register() strips leading/trailing whitespace from the username before
storing it; login() was not, so a user who typed " alice " at login
would get a 401 even though their account existed as "alice". Now both
handlers trim consistently.
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>
Adds three new challenge types (win in 3 min, score ≥5000, win in 8 min)
so the daily challenge rotates through a fortnight of variety before any
variant repeats. Seeds are still deterministic worldwide.
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>
Username: 3–32 chars, alphanumeric + underscore only.
Password: minimum 8 characters.
Both return HTTP 400 Bad Request with a human-readable message.
Adds three integration tests for the new validation rules.
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>
Adds 4 more rounds of 5 seeds each, organized by difficulty tier.
Seeds wrap modulo list length so the pool grows non-destructively.
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>
Adds delete_account() as a default no-op on the SyncProvider trait.
SolitaireServerClient sends DELETE /api/account with JWT (retry on 401).
The server handler already existed (DELETE /api/account, ON DELETE CASCADE).
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>
When a user pushes sync data and is opted in to the leaderboard, the
server now updates their leaderboard row with the merged stats using
MAX(best_score) and MIN(best_time_secs) — scores never regress even if
the client sends stale data.
Eliminates the need for a separate score-submission API call: the sync
push already carries the full stats, so the leaderboard stays current
after every push.
Added two integration tests:
- push_after_opt_in_updates_leaderboard_score
- push_lower_score_does_not_overwrite_leaderboard_best
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>