Two follow-up commits on top of v0.16.0: -87275bf: H-key hint asks the v0.15.0 solver for the actual best first move, with the existing heuristic kept as fallback. -53e3b81: Settings → Gameplay slider tunes replay playback rate (0.10–1.00 s, default 0.45 s) read per frame from SettingsResource. Adds the [0.17.0] CHANGELOG section, folds the post-v0.16.0 provisional table into a v0.17.0 shipped table in SESSION_HANDOFF, prunes the now-stale "Cut v0.17.0" item from the punch list, and re-letters the resume-prompt decision options A–D. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
27 KiB
Changelog
All notable changes to Solitaire Quest are documented here. The format is based on Keep a Changelog and this project follows Semantic Versioning.
Unreleased
Nothing yet.
[0.17.0] — 2026-05-06
A short follow-up round on top of v0.16.0: the H-key hint is no longer a heuristic guess but the actual best first move suggested by the v0.15.0 solver, and the in-engine replay player now has a player-tunable playback rate.
Added
- Replay-rate slider in Settings → Gameplay. Tunes
replay_move_interval_secsfrom 0.10 s to 1.00 s in 0.05 s steps; default 0.45 s.tick_replay_playbackreads the value fromSettingsResourceper frame so the slider takes effect on the next playback tick — no restart required.
Changed
- Solver-driven hints. Pressing H used to surface a
heuristic-best move (foundation moves preferred, then
tableau-to-tableau by depth-of-flip-revealed). It now asks the
v0.15.0 solver for the actual provably-best first move via the
new
solitaire_core::solver::try_solve_with_first_move/try_solve_from_stateAPIs. When the solver returns inconclusive (rare deals where the bound runs out before a result), the old heuristic remains the fallback. Median 2 ms per H press.
Stats
- 1208 passing tests (was 1196 at v0.16.0 close).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.16.0 — 2026-05-06
A modal-feel polish round. Every overlay screen now scrolls when its content overflows the 800×600 minimum window, every clickable button shows a hand cursor on hover, keyboard focus lands on the primary button on the same frame the modal opens, and read-only modals dismiss when the player clicks the scrim outside the card.
Added
- Pointer cursor on hover for every interactive
Buttonentity (modal buttons, HUD action bar, mode-launcher cards, settings toggles, Stats selectors).update_cursor_icongains a fourth branch sitting between Grabbing (active drag) and Grab (draggable card hover): when no drag is active and anyInteraction::Hovered/Pressedbutton is detected, the window cursor swaps toSystemCursorIcon::Pointer. A purepick_cursor_iconhelper makes the priority logic unit-testable. - Click-outside-to-dismiss for the six read-only modals: Stats,
Achievements, Help, Profile, Leaderboard, Home. New
ScrimDismissiblemarker onModalScrimopts a modal in;dismiss_modal_on_scrim_clickruns inUpdate, despawns the topmost dismissible scrim on a left-mouse press whose cursor lands on the scrim and outside everyModalCard. Bevy's hierarchy despawn cascades to the card and children. Settings, Onboarding, Pause, Forfeit confirm, and Confirm New Game intentionally don't opt in — they carry unsaved or destructive state.
Fixed
- Modal content scrolls when it overflows (Achievements, Help,
Stats, Profile, Leaderboard). Each modal's body Node now
carries
Overflow::scroll_y()plus amax_heightconstraint (Val::Vh(70.0)for most,Val::Vh(50.0)for the leaderboard's variable-length ranking section) and a marker component (AchievementsScrollable,HelpScrollable,StatsScrollable,ProfileScrollable,LeaderboardScrollable). A siblingscroll_*_panelsystem per modal routesMouseWheelevents into the body'sScrollPosition. Mirrors the existingSettingsPanelScrollablepattern. Home modal intentionally not scrolled — its five mode cards + Cancel are sized to fit at 800×600 by design. - Modal focus arrives on the same frame the modal opens.
Previously
attach_focusable_to_modal_buttonsandauto_focus_on_modal_openran inUpdatealongside arbitrary click-handlers that spawn modals; with no ordering edge, Bevy's deferredCommandsqueued the new entities but the attach system couldn't see them on the same tick. Both systems moved toPostUpdateso the schedule boundary itself supplies the sync point —FocusedButtonis always populated beforeapp.update()returns. The very next Tab/Enter press lands on a populated resource instead of wasting itself moving focus from None to the primary.
Stats
- 1196 passing tests (was 1178 at v0.15.0 close).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.15.0 — 2026-05-02
In-engine replay playback, the Klondike solver + "Winnable deals only" toggle, a 19th achievement, rolling replay history, and a significant build-time / binary-size win from disabling Bevy's default audio stack.
Added
- In-engine replay playback for the Stats overlay's Watch Replay
button. New
ReplayPlaybackPluginruns a state machine (Inactive / Playing / Completed) that resets the live game to the recorded deal and ticks throughreplay.movesatREPLAY_MOVE_INTERVAL_SECS(0.45 s) firing the canonicalMoveRequestEvent/DrawRequestEventper recorded move. Recording is suppressed during playback so replays don't re-record themselves. - Replay overlay banner (
ReplayOverlayPlugin) anchored to the top of the window during playback. Shows "Replay" label, "Move N of M" progress, and a Stop button. Z-order leaves modals (Settings, Pause, Help) free to render on top so the player can adjust audio mid-replay. - Rolling replay history at
<data_dir>/replays.jsoncapped at 8 entries. Replaces the single-slotlatest_replay.json(legacy file is migrated forward on first launch viamigrate_legacy_latest_replay). Stats overlay gains a Prev / Next selector and a "Replay N / M" caption so the player can revisit older wins. - "Cinephile" achievement (#19). Unlocks the first time
ReplayPlaybackStatetransitions Playing → Completed (i.e. the replay played out to its end without the player pressing Stop). Stop transitions Playing → Inactive directly so it doesn't count. - Klondike solver in
solitaire_core::solver. Iterative-DFS with memoisation on a 64-bit canonical state hash, two budget knobs (move_budget + state_budget) for pathological cases, and a three-stateSolverResult(Winnable / Unwinnable / Inconclusive). Median solve time 2 ms; pathological inconclusives cap near 120 ms. Pure logic —solitaire_corekeeps no Bevy or I/O. - "Winnable deals only" toggle in Settings → Gameplay (default
off). When on,
handle_new_gamewalks seed N, N+1, N+2, … throughtry_solveuntil it finds Winnable or Inconclusive, capped atSOLVER_DEAL_RETRY_CAP(50) attempts. Daily challenges, replays, and explicit-seed requests bypass the solver — only random Classic deals are gated.
Changed
- Bevy default-feature trim (
bevy = { default-features = false, features = [...] }in workspace Cargo.toml) drops 51 transitive crates including thebevy_audio→ rodio → cpal 0.15 + symphonia chain that the project doesn't use (kira handles audio directly). The retained feature list is curated to exactly what the engine uses;solitaire_wasmis unaffected because it doesn't depend on bevy.
Stats
- 1178 passing tests (was 1134 at v0.14.0 close).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.14.0 — 2026-05-02
Two threads land in v0.14.0: the second half of the post-v0.12.0 UX candidate list (theme thumbnails, daily-challenge calendar, Time Attack auto-save, per-mode bests, time-bonus multiplier) plus a major new feature — the replay pipeline (record → upload → web viewer). Three Quat-reported bugs from a smoke-test round shipped alongside.
Added
- Theme-picker thumbnails in Settings → Cosmetic. Each theme chip
renders a small Ace-of-Spades + back preview pair via the existing
rasterize_svgpath. Cached per theme in a newThemeThumbnailCache. Themes that lack a preview SVG fall back to a transparent placeholder rather than crashing. - 14-day daily-challenge calendar in the Profile modal. Horizontal
row of dots showing the trailing two weeks; today's dot is ringed
in
ACCENT_PRIMARY, completed days fillSTATE_SUCCESS, missed days fillBG_ELEVATED. Caption above the row reads "Current streak: N · Longest: M". - Time Attack session auto-save to
<data_dir>/time_attack_session.json, atomic .tmp + rename. 30-second auto-save while a session is active, plus onAppExit. Sessions whose 10-minute window expired in real time while the app was closed are discarded on load. Classic, Zen, and Challenge already auto-saved correctly viagame_state.json— Time Attack was the only mode missing session-level persistence. - Per-mode best-score and fastest-win readouts in the Stats screen.
StatsSnapshotgains six#[serde(default)]fields (Classic / Zen / Challenge × best_score + fastest_win_seconds). Stats screen renders a "Per-mode bests" section between the primary cell grid and progression. Lifetime totals continue to roll all modes together. - Time-bonus multiplier slider in Settings → Gameplay (0.0–2.0, 0.1 steps, default 1.0, "Off" label at zero). Cosmetic only — multiplies the time-bonus shown in the win modal but does NOT affect achievement unlock thresholds (those still use the raw unmultiplied score).
- Win-replay recording + storage. Every move during a successful
game appends to a
RecordingReplayresource; onGameWonEventthe recording freezes into aReplay(seed + draw_mode + mode + score + time + ordered move list) and persists to<data_dir>/latest_replay.jsonatomically. Single-slot — overwrites on every win. - "Watch replay" button in the Stats overlay. Shows the latest
win's caption and surfaces a button that loads the replay (button
fires an
InfoToastEventdescribing the replay; full in-engine playback is deferred to a future build). - Replay upload + fetch endpoints on the server.
POST /api/replaysaccepts aReplayJSON;GET /api/replays/:idreturns it. JWT-gated with the existing auth middleware. Engine uploads winning replays automatically when the player has cloud sync configured. solitaire_wasmcrate — new workspace member compiling replay-relevantsolitaire_coretypes to WebAssembly so a browser can re-execute a replay client-side. No-std-friendly surface;wasm-bindgenglue.- Web replay viewer served from the Solitaire server.
GET /replays/:idreturns HTML + CSS + the wasm bundle that fetches the replay JSON, rasterises a deal from the seed, and animates the recorded moves. - Card flight animations on the web side so the browser viewer reads as a real game replay rather than a static dump.
Fixed
- Multi-card lift validation.
solitaire_core::rules::is_valid_tableau_sequencerejects a moved stack whose adjacent cards don't form a descending alternating-colour run. Previously a player could lift any multi-card selection and drop it as long as the bottom landed legally. Wired intomove_cards's tableau-destination branch. - Softlock detection.
has_legal_movesrewritten to walk every potential move source (every stock card, every waste card, the face-up top of every tableau column) and check it against every foundation and every tableau. Previously the heuristic early-returnedtruewhenever stock had cards — players got stuck in unwinnable end-states with no end-game screen.GameOverScreennow actually fires for true softlocks. Quat's exact reproduction case is pinned by a new test. - Deal-tween information leak. New-game now snaps every card
sprite to the stock pile position before writing
StateChangedEvent, so all 52 cards animate from a single point during the deal. Previously the sprites started from their previous-game positions, briefly revealing the prior deal.
Documentation
SESSION_HANDOFF.mdrefreshed for the Quat smoke-test round including investigation findings on solver decisions and dependency duplicates.
Stats
- 1134 passing tests (was 1053 at v0.13.0 close).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.13.0 — 2026-05-02
Third UX iteration round on top of v0.12.0. Six handoff candidates shipped — three small polish items, three larger interaction features (theme-aware backs, full keyboard play, right-click power shortcut). Plus two code-review fixes (font handling unified, sccache wiring removed).
Added
- Tooltip-delay slider in Settings → Gameplay.
tooltip_delay_secsranges [0.0, 1.5] in 0.1 s steps; "Instant" label when zero.Settings.tooltip_delay_secsround-trips through serialise/deserialise with#[serde(default)]. The hover-delay comparison inui_tooltipreads fromSettingsResourcewith the existingMOTION_TOOLTIP_DELAY_SECSas the test-fixture fallback. - Win-streak fire animation. New
WinStreakMilestoneEventfires fromstats_pluginwhenwin_streak_currentcrosses any of [3, 5, 10] (only the threshold crossing — not every subsequent win). The HUD streak readout scale-pulses 1.0 → 1.20 → 1.0 overMOTION_STREAK_FLOURISH_SECS(0.6 s). - Score-breakdown reveal on the win modal. Replaces the single
"Score: N" line with a per-component reveal (Base / Time bonus /
No-undo bonus / Mode multiplier / Total). Rows fade in over
MOTION_SCORE_BREAKDOWN_FADE_SECS(0.12 s) staggered byMOTION_SCORE_BREAKDOWN_STAGGER_SECS(0.15 s). HonoursAnimSpeed::Instantby spawning all rows fully visible. - Card backs follow the active theme.
theme.ron'sbackslot now actually drives the face-down sprite. Active-theme back rasterises alongside the faces and supersedes the legacyback_N.pngpicker. The picker remains as a fallback for themes that don't ship a back, and the Settings UI surfaces a caption ("Active theme provides its own back") + dimmed swatches when the override is in effect. - Keyboard-only drag-and-drop. Tab cycles draggable card stacks,
Enter "lifts" the focused stack, arrow keys (or Tab) cycle the
legal-destination targets only, Enter confirms, Esc cancels. A
new
KeyboardDragStateresource models the two-mode flow without changing the existingSelectionStatecontract. Mutual exclusion with mouse drag uses a sentinelDragState.active_touch_id = KEYBOARD_DRAG_TOUCH_ID(u64::MAX) so neither pipeline can trample the other. - Right-click radial menu. Hold right-click on a face-up card →
a small ring of icons appears at the cursor with one entry per
legal destination. Release over an icon → fires
MoveRequestEvent; release in dead space, Esc, or left-click cancels. Skips the drag motion entirely. NewRadialMenuPluginowns the flow; co-exists with the existingRightClickHighlightpile-marker tint.
Fixed
- Font handling consolidated to bundled-only. Code-review
feedback: the SVG rasteriser previously mixed
load_system_fonts+ bundled FiraMono + a lenient resolver, which made card text rendering depend on host fontconfig. Picked option (a) and applied it across both layers —font_pluginnow embedsassets/fonts/main.ttfviainclude_bytes!()and registers it withAssets<Font>;svg_loader::shared_fontdbloads only the bundled bytes; the newbundled_font_resolverignores the SVG'sfont-familyrequest and always returns the single bundled face. A parse failure aborts with a clear error ("bundled FiraMono failed to parse — binary is corrupt").
Removed
- Project-level sccache wiring. Code-review feedback: sccache
shouldn't be a per-project build dependency. Cargo's incremental
cache already covers the single-project case, and forcing
rustc-wrapper = "sccache"workspace-wide meant every contributor had to install it..cargo/config.tomldeleted entirely; plaincargo buildnow works without setup.
Documentation
help_plugincontrols reference gains a "Mouse" section covering double-click auto-move, right-click highlight, and the new hold-RMB radial.help_pluginalso gains a "Keyboard drag" section for the new Tab/Enter/Arrows/Esc flow.- Onboarding slide 3 picks up a
Tab → Enterrow referencing the full keyboard drag path.
Stats
- 1053 passing tests (was 1031 at v0.12.0 close).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.12.0 — 2026-05-02
UX feel polish round on top of v0.11.0. Six small-but-tangible improvements that make the play surface feel more responsive, forgiving, and discoverable, plus the doc refresh that should have ridden along with v0.11.0.
Added
- Foundation completion flourish. When a King lands on a
foundation (Ace-through-King for that suit), a brief celebration
fires: King card scale-pulses 1.0 → 1.15 → 1.0 over 0.4 s, the
foundation marker tints
STATE_SUCCESSfor the first half then fades, and a synthesised C6→E6→G6 bell ping plays (~240 ms, octave abovewin_fanfare's root so the fourth completion + win cascade layer cleanly). NewFoundationCompletedEvent { slot, suit }carries the trigger so future systems can hook in. - Drag-cancel return tween. Illegal drops glide each dragged
card back to its origin slot over 150 ms with a quintic ease-out
curve (
MotionCurve::Responsive, zero overshoot — reads forgiving rather than jittery). The audio cue (card_invalid.wav) still fires for negative feedback. Right-click and double-click invalid paths still useShakeAnimsince there's no motion to interpolate. - Focus ring breathing. The keyboard focus ring's alpha modulates
with a 1.4 s sin curve over [0.65, 1.0] of its native value so the
indicator catches the eye on focus changes without competing with
gameplay. Honours
AnimSpeed::Instantby reverting to the static outline for reduced-motion users. - First-win achievement onboarding toast. After the player's
very first win, a one-shot info toast surfaces "First win! Press
A to see your achievements."
Settings.shown_achievement_onboardingpersists the seen state so the cue never re-fires (legacysettings.jsonfiles load tofalsevia#[serde(default)]). - Mode Launcher digit shortcuts. Pressing M opens the Home modal (the Mode Launcher); inside it, pressing 1–5 launches each mode directly without needing Tab + Enter. Locked modes (Zen, Challenge, Time Attack at level < 5) are silent no-ops. Modal-scoped — digit keys outside the launcher fire nothing.
Fixed
- Card aspect ratio matches hayeah SVGs.
CARD_ASPECT1.4 → 1.4523 to match the bundled artwork's natural 167.087 × 242.667 dimensions. Cards previously rendered ~3.6 % vertically squashed. The vertical-budget math incompute_layoutusesCARD_ASPECTalgebraically so the worst-case-tableau-fits-on-screen guarantee adapts automatically.
Documentation
- README refresh with v0.11.0+ features (card themes, HUD overhaul, drag feel, unlocked foundations) and a corrected controls table — the previous table inverted Z/U for undo and listed H for help when F1 is the binding.
- CHANGELOG.md added (this file), covering v0.9.0–v0.12.0 with Keep a Changelog 1.1.0 conventions.
Stats
- 1007 passing tests (was 982 at v0.11.0).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.11.0 — 2026-05-02
The biggest release since 0.10.0. Headline threads: a runtime card-theme system, an HUD restructure that reclaims the play surface, and a round of UX feel polish surfaced by smoke testing.
Added
- Runtime card-theme system (CARD_PLAN phases 1–7).
- Bundled default theme ships in the binary via
embedded://— 52 hayeah/playing-cards-assets SVGs (MIT) plus a midnight-purpleback.svgas original work. - User themes live under
themes://rooted atuser_theme_dir(). Drop a directory containingtheme.ron+ 53 SVGs and the registry picks it up on next launch. - Importer at
solitaire_engine::theme::import_theme(zip)validates archives (20 MB cap, zip-slip rejection, manifest validation, every SVG round-tripped through the rasteriser) and atomically unpacks. - Picker UI in Settings → Cosmetic; selection persists as
selected_theme_idand propagates to live sprites.
- Bundled default theme ships in the binary via
- Reserved HUD top band (64 px) so cards no longer crowd the score
readout or action buttons; layout's
top_yshifts down accordingly. - Action-bar auto-fade — buttons fade out when the cursor leaves the band, fade back in when it returns. Lerp at ~167 ms.
- Visible drop-target overlay during drag — a soft fill plus 3 px outline drawn ABOVE stacked cards for every legal target (full fanned column for tableaux, card-sized for foundations and empty tableaux). Replaces the previously invisible pile-marker tint.
- Card drop shadows — every card casts a neutral 25 % black shadow with a 4 px halo; cards in the active drag set switch to a lifted shadow (40 % alpha, larger offset, bigger halo).
- Stock remaining-count badge — small
·Nchip at the top-right of the stock pile so the player can see how close they are to a recycle. Hides when the stock empties.
Changed
- Foundations are unlocked.
PileType::Foundation(Suit)→Foundation(u8)(slot 0..3). The claimed suit is derived from the bottom card viaPile::claimed_suit()— no separate field, no claim-stuck-after-undo bugs. Any Ace lands in any empty slot, and the slot then claims that suit.next_auto_complete_moveprefers a claim-matched slot before falling back to the first empty slot for Aces. Empty foundation markers render as plain placeholders (no "C/D/H/S"). - HUD selection label and hint toast read
claimed_suit()and fall through to "Foundation N" / "move to foundation" only when the slot is empty.
Fixed
shared_fontdbnow bundles FiraMono. The hayeah SVGs referenceBitstream Vera SansandArialby name. On minimal Linux installs / fresh Wayland sessions / chroots where neither is installed AND the CSS-generic aliases don't resolve, card rank/suit text vanished. The bundled font is loaded into fontdb and pinned as every CSS generic's target so the resolver always lands on something real. Surfaced when a second-machine pull rendered cards without glyphs.- Theme asset path resolution —
AssetPath::resolve(concatenates) →resolve_embed(RFC 1808 sibling resolution). Was producing paths like…/theme.ron/hearts_4.svgand failing to load every face SVG. - Sync exit log spam —
push_on_exitsilently no-ops onLocalOnlyProvider'sUnsupportedPlatforminstead of warn-spamming every shutdown. - usvg font-substitution warn spam — custom
FontResolver.select_fontappendsFamily::SansSerifandFamily::Serifto every query so unmatched named families silently fall through.
Migration
- In-progress saves invalidated.
GameState.schema_versionbumped 1 → 2; pre-v2game_state.jsonfiles silently fall through to "fresh game on launch." Stats, progress, achievements, and settings live in separate files and are unaffected.
Stats
- 982 passing tests (was 819 at v0.10.0).
- Zero clippy warnings under
--workspace --all-targets -- -D warnings.
0.10.0 — 2026-04-29
PNG art pipeline plus a major dependency pass. The first release where the binary shipped with bundled artwork.
Added
- 52 individual card face PNGs generated via
solitaire_assetgen. - Custom font (FiraMono-Medium) loaded via
AssetServerat startup through the newFontPlugin. - Card backs and backgrounds upgraded to 120×168 with richer patterns.
- Ambient audio loop wired through the kira mixer.
- Arch Linux PKGBUILDs for the game client and sync server (under
the separate
solitaire-quest-pkgbuilddirectory). - Workspace README, CI workflow, migration guide.
Changed
- Bevy 0.15 → 0.18 workspace migration.
- kira 0.9 → 0.12 audio backend migration.
- Edition 2024, MSRV pinned to Rust 1.95.
- rand 0.9 upgrade.
- Card rendering moved from
Text2doverlay to PNG-backedSpritewith face/back atlases;Text2dretained as a headless fallback whenCardImageSetis absent (tests under MinimalPlugins). - Asset pipeline switched from
include_bytes!()for PNGs/TTFs to runtimeAssetServer::load()so artwork can be swapped without a recompile. Audio remains embedded. - Removed Google Play Games Services sync backend — redundant with the self-hosted server.
Fixed
- Server JWT secret loaded at startup (was lazy, surfaced as intermittent 500s).
- Daily-challenge race in the server's seed-generation path.
- Rate limiter switched to
SmartIpKeyExtractorso the limit applies per real client IP rather than per upstream proxy. - Touch input uses
MessageReader<TouchInput>(Bevy 0.18 rename). - Sync push/pull races in async task scheduling.
- Hot-path allocations reduced in card-rendering systems.
- Conflict report coverage added for sync merge edge cases.
Stats
- 819 passing tests at tag time.
0.9.0 — 2026-04-28
Initial public-tagged release. Established the workspace structure
(solitaire_core / _sync / _data / _engine / _server / _app /
_assetgen), the modal scaffold via ui_modal, the design-token system
in ui_theme, and the four-tier HUD layout. Foundations were
suit-locked at this point; cards rendered as Text2d rank/suit overlays
with no PNG artwork yet.
Added
- Klondike core (Draw One / Draw Three modes).
- Progression system (XP, levels, 18 achievements, daily challenge, weekly goals, special modes at level 5).
- Self-hosted sync server (Axum + SQLite + JWT auth).
- All 12 overlay screens migrated to the
ui_modalscaffold with real Primary/Secondary/Tertiary buttons. - Animation upgrades:
SmoothSnapslide curves, scoped settle bounce, deal jitter, win-cascade rotation. - Splash screen, focus rings (Phases 1–3), tooltips infrastructure + HUD/Settings/popover applications, achievement integration tests, destructive-confirm verb unification, leaderboard error/idle states, first-launch empty-state polish, hit-target accessibility fix, CREDITS.md, persistent window geometry, mode-launcher Home repurpose, client-side sync round-trip integration tests.