36 Commits

Author SHA1 Message Date
funman300 95df5421c9 feat(core): unlock foundations — Foundation(u8) slots, suit derived from contents
Standard Klondike behaviour: any Ace can land in any empty foundation,
and that slot then claims the suit until the pile empties. The
previous PileType::Foundation(Suit) variant pre-assigned each of the
four foundations to a fixed suit ("C / D / H / S" placeholders) and
rejected mismatched Aces — non-standard and (per the smoke-test
feedback) confusing.

Replaces the variant payload with a slot index Foundation(u8) (0..=3)
and derives the claimed suit from the bottom card via a new
Pile::claimed_suit() method. The bottom card is, by construction,
the Ace that established the claim; using it directly eliminates an
entire class of "stuck claim after undo" bugs that a separate
claimed_suit field would have introduced.

can_place_on_foundation drops its suit parameter — the rule reduces
to "empty pile accepts any Ace; non-empty pile accepts the next
rank up of the bottom card's suit." Iteration sites across
input_plugin, cursor_plugin, selection_plugin, card_plugin,
auto_complete_plugin, game_plugin, layout, and hud_plugin all swap
the four-suit list for `(0..4u8).map(PileType::Foundation)`.

next_auto_complete_move now prefers a slot whose claimed_suit matches
the candidate card before falling back to the first empty slot for
an Ace — so the same suit consistently auto-targets the same slot
across the whole game, matching player expectations.

The HUD selection label and the hint toast read claimed_suit() and
fall back to "Foundation N" / "move to foundation" only when the
slot is empty. Empty foundation pile markers no longer render the
suit-letter children — they're plain translucent rectangles, matching
empty tableau placeholders.

Save-format invalidation: GameState gains a schema_version field
(serde-default to 1 for back-compat parsing of old files), the
constant is bumped to 2, and load_game_state_from rejects mismatched
schemas. Old in-progress saves silently fall through to "fresh game
on launch" — the user accepted this loss given the mechanic change.
Stats / progress / achievements / settings live in separate files,
contain no PileType data, and are unaffected.

9 new tests pin the contract:
- Pile::claimed_suit returns None for empty / non-foundation, Some
  for non-empty foundation
- Any Ace lands in the first empty foundation; successive Aces
  distribute across slots 0..3
- Claim drops when the slot is emptied via undo
- Auto-complete picks the slot with a matching claim, not the first
  empty slot
- A v1-format game_state.json is rejected; sibling stats save/load
  is unaffected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:17:17 +00:00
funman300 924a1e2af7 feat(engine): card-theme picker in Settings → Cosmetic
CI / Test & Lint (push) Failing after 5s
CI / Release Build (push) Has been skipped
Wires the runtime theme system (CARD_PLAN.md phases 1–7) into the
visible Settings UI so a player can switch between every theme
discovered by `ThemeRegistry` without restarting.

solitaire_data/src/settings.rs
  Settings gains `selected_theme_id: String` (default "default"),
  guarded by `#[serde(default = "default_theme_id")]` so existing
  settings.json files deserialize cleanly.

solitaire_engine/src/settings_plugin.rs
  - SettingsButton::SelectTheme(String) variant + focus order 85.
  - sync_settings_panel_visibility now reads
    Option<Res<ThemeRegistry>>, snapshots id+display_name pairs, and
    threads them into spawn_settings_panel. When the registry is
    absent (tests under MinimalPlugins) the picker silently skips —
    every existing test continues to pass unchanged.
  - theme_picker_row helper: like picker_row but keyed by String
    rather than usize, with chips wide enough for theme display
    names. Attaches the canonical tooltip ("Choose card-face
    artwork. Imported themes appear here.") and the FocusRow marker
    so Left/Right arrows cycle within the row.
  - Click handler updates settings.selected_theme_id, persists, and
    fires SettingsChangedEvent — same shape as every other picker.

solitaire_engine/src/theme/plugin.rs
  - load_default_theme renamed to load_initial_theme; reads
    SettingsResource on Startup and seeds ActiveTheme from
    settings.selected_theme_id (falling back to embedded default).
  - react_to_settings_theme_change watches SettingsChangedEvent,
    no-ops when the active theme already matches, and otherwise
    swaps ActiveTheme — the existing
    sync_card_image_set_with_active_theme system then refreshes
    every card sprite on the next AssetEvent::LoadedWithDependencies.

cargo build / clippy --workspace --all-targets -- -D warnings / test
--workspace all green (960 passed, 0 failed, 9 ignored).
2026-05-01 16:24:24 +00:00
funman300 3ef4ecb747 test(data): client-side sync round-trip integration tests
CI / Test & Lint (push) Failing after 6m45s
CI / Release Build (push) Has been skipped
Server-side endpoint tests already exist in solitaire_server. This
adds the client-side counterpart: five integration tests in
solitaire_data/tests/sync_round_trip.rs that drive
SolitaireServerClient against an in-process axum::serve harness with
an in-memory SQLite database, covering:

- register_login_push_pull_round_trip — happy path: register, push
  non-default stats, pull from a fresh client, assert the merged
  payload reflects the pushed values
- pull_after_concurrent_pushes_merges_correctly — two clients on one
  user push different games_played values, verify the server-side
  merge returns the max
- unauthenticated_pull_returns_authentication_error — pull without
  tokens surfaces SyncError::Auth as expected
- jwt_refresh_on_401_succeeds — replace the access token with one
  whose exp is two hours stale (same signing key), pull triggers
  401 → /api/auth/refresh → retry, asserts the call ultimately
  succeeds
- pull_after_account_deletion_returns_default_or_error — register,
  push, delete via the trait, confirm the next push surfaces a
  result rather than panicking

keyring_core's mock store is installed once per process via Once;
each test uses a unique username so the shared store doesn't
cross-contaminate. Production code in sync_client.rs needed no
changes — the Box<dyn SyncProvider> design plus the mock keyring
were sufficient to drive every flow from outside.

solitaire_server is added as a path dev-dependency along with the
direct crates the harness needs (axum, sqlx, jsonwebtoken, uuid,
chrono, solitaire_sync); no runtime deps changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:46:48 +00:00
funman300 4b9d008be2 refactor(workspace): sweep low-risk clippy::pedantic findings
Conservative cleanup pass — applied only the high-signal pedantic
lints whose fixes either remove genuine waste or read more naturally,
skipping anything stylistic that would bloat the diff.

- map_unwrap_or: 29 .map(...).unwrap_or(...) sites collapsed to
  .map_or / .is_some_and / .map_or_else equivalents
- uninlined_format_args: 7 production format!/write!/println! sites
  rewritten to the inline-argument style; assert! sites in test code
  intentionally untouched
- match_same_arms: 2 redundant arms collapsed where the bodies were
  identical and the merger didn't obscure intent

Public API is unchanged. No dependencies added or removed. The
pedantic warning count dropped from 840 to 807 (-33). Out-of-scope
findings — needless_pass_by_value on Bevy Res params, false-positive
explicit_iter_loop on Bevy Query iterators, items_after_statements
inside test mods, and the "ask before changing" merge logic in
solitaire_sync — were intentionally deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:46:32 +00:00
funman300 6e7705b256 feat(app): persist window geometry across launches
Settings gains an optional window_geometry field (size + position)
serialized via #[serde(default)] so legacy settings.json files without
the field deserialize cleanly to None. On launch the app restores
the persisted dimensions and position; first run and pre-upgrade
saves keep the existing 1280x800 centered default.

settings_plugin records changes from WindowResized and WindowMoved
into a PendingWindowGeometry resource and writes them to disk through
the existing atomic .tmp+rename path once the events have stayed
quiet for WINDOW_GEOMETRY_DEBOUNCE_SECS (0.5s). A merge_geometry
helper preserves whichever component (size or position) the latest
event burst didn't carry, so a position-only WindowMoved never wipes
the recorded size.

Pure should_persist_geometry and merge_geometry helpers are unit
tested for the boundary cases. Headless integration tests cover the
full flow: a single resize event then a quiet window persists, a
move event after a resize updates only position, a rapid storm
collapses to the final size, and a quiet frame with no events
leaves the geometry untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:17:54 +00:00
funman300 79d391724e chore(data): derive Copy on AnimSpeed
AnimSpeed is a fieldless enum; adding Copy lets `scaled_duration`
and other helpers take it by value through `&AnimSpeed` deref
without requiring a `.clone()` at every call site. Prerequisite
for the upcoming animation token-routing work.
2026-04-30 04:26:57 +00:00
funman300 18ac5adef5 feat(engine): art pass — PNG assets, custom font, and keyring v4 upgrade
Art pass (Phase 4):
- Generate placeholder PNG assets: face.png, back_0–4.png, bg_0–4.png via
  solitaire_assetgen gen_art binary (16×16 RGBA, embedded via include_bytes!)
- Add FiraMono-Medium font (assets/fonts/main.ttf) embedded at compile time
- Add FontPlugin: loads font at startup, exposes FontResource; gracefully
  falls back to default handle when Assets<Font> absent (MinimalPlugins tests)
- Wire CardImageSet into card_plugin: face/back PNGs replace solid-colour
  sprites when available; tests continue using colour fallback via MinimalPlugins
- Wire BackgroundImageSet into table_plugin: bg PNGs replace solid-colour
  background; empty set inserted when Assets<Image> absent in tests
- Fix hint highlight system (input_plugin): tint sprite.color directly instead
  of replacing the whole Sprite (which would discard the image handle)
- Export FontPlugin, FontResource, CardImageSet from solitaire_engine::lib
- Register FontPlugin in solitaire_app before other plugins

Dependency upgrades (latest releases):
- keyring "2" → keyring "4" + keyring-core "1" (v4 split architecture into
  separate core library crate)
- auth_tokens.rs: Entry::new now returns Result; delete_password →
  delete_credential; NoDefaultStore error variant handled
- solitaire_app: add keyring::use_native_store(true) at startup for Linux
  Secret Service / macOS Keychain / Windows Credential Store selection

ARCHITECTURE.md: fix Edition 2025→2021, update asset pipeline section,
add FontPlugin/CardImageSet/BackgroundImageSet to plugin and resource tables,
update Section 14 to reflect actual include_bytes!() rendering approach,
add Decision Log entries for embedded PNG and font decisions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:30:55 +00:00
funman300 41d75b50de feat/fix/perf(engine,data,assetgen): ambient audio, sync bug fixes, hot-path cleanup
**ambient_loop.wav (task 5)**
- solitaire_assetgen: add ambient_loop() synthesizer — 5 s seamless loop,
  55 Hz drone with 2nd/3rd harmonics, 0.2 Hz LFO breath, 16-bit mono 44100 Hz
- audio_plugin: load ambient_loop.wav via include_bytes!() replacing the
  card_flip.wav placeholder; decouple start_ambient_loop() from SoundLibrary

**sync bug fixes (task 11)**
- sync_plugin: LocalOnlyProvider returning UnsupportedPlatform now sets
  SyncStatus::Idle instead of displaying a misleading "Sync not configured" error
- sync_client: extract_pull_body / extract_push_body now return SyncError::Auth
  only for HTTP 401/403; all other non-2xx statuses return SyncError::Network
- sync_plugin: push_on_exit now logs a warn! on failure instead of silently
  discarding the result

**hot-path performance (task 12)**
- card_plugin: card_positions() now returns &Card references (lifetime-bound to
  GameState) instead of owned Card clones — eliminates 52 Card clones per
  sync_cards() call (runs every animation frame)
- input_plugin: card_position() takes &PileType instead of PileType, eliminating
  PileType copies at every drag hit-test call site
- animation_plugin: eliminate intermediate AnimSpeed clone in handle_win_cascade()

**docs (tasks 11, 13)**
- docs/sync_test_runbook.md: manual test runbook for cross-machine sync
- docs/android_investigation.md: cargo-mobile2 port investigation and effort estimate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:51:58 +00:00
funman300 4bd562671e chore(data,engine,docs): remove Google Play Games Services sync backend
Deletes the solitaire_gpgs crate and all GPGS references from settings,
sync client, profile plugin, CLAUDE.md, and ARCHITECTURE.md. The
self-hosted server covers all sync needs without the Android-only backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:22:25 +00:00
funman300 800dfb50ce chore(pkg): add Arch Linux PKGBUILDs for game client and sync server
- pkg/solitaire-quest/PKGBUILD: builds solitaire_app binary, depends on
  alsa-lib, libxkbcommon, systemd-libs (Bevy Linux requirements); check()
  runs only non-Bevy crates (solitaire_core, solitaire_sync) since Bevy
  integration tests require a GPU/display unavailable in chroot
- pkg/solitaire-quest-server/PKGBUILD: builds solitaire_server binary,
  installs systemd service unit and hardened environment file template
- pkg/solitaire-quest-server/solitaire-quest-server.service: systemd unit
  with ProtectSystem=strict, NoNewPrivileges, dedicated service user
- pkg/solitaire-quest-server/server.env: documented env template installed
  to /etc/solitaire-quest-server/server.env (mode 0640, listed in backup=)
- LICENSE: add MIT license
- Cargo.toml: add license = "MIT" to [workspace.package]
- All member crates: add license.workspace = true

Both PKGBUILDs follow the Arch Rust package guidelines:
  prepare() uses --locked + cargo fetch
  build() uses --frozen --release -p <crate>
  RUSTUP_TOOLCHAIN=stable and CARGO_TARGET_DIR=target set in each stage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:44:44 +00:00
funman300 ffc79447d4 fix+refactor+docs: P0–P3 todo list items
P0 fixes:
- Register WinSummaryPlugin, SelectionPlugin, CardAnimationPlugin in main.rs
  (all three were exported but never wired — features silently did nothing)
- game_state::draw(): increment move_count on waste→stock recycle, not just
  on normal draws; add move_count_increments_on_recycle regression test

P1 fixes:
- solitaire_server/Cargo.toml: remove duplicate dev-dependencies
  (solitaire_sync, uuid, chrono, jsonwebtoken were in both sections)

P2 — input_plugin refactor:
- Split 198-line handle_keyboard() into three focused systems under 110 lines each:
  handle_keyboard_core (U/N/Z/D/Space), handle_keyboard_hint (H), handle_keyboard_forfeit (G)
- Introduce KeyboardConfirmState resource to share countdown timers across systems
- Add three new unit tests: all_hints_suggests_draw_*, all_hints_is_empty_when_truly_stuck,
  new_game_confirm_window_is_positive

P2 — achievement predicate tests (solitaire_core):
- Add 10 direct unit tests for speed_demon, lightning, no_undo, high_scorer,
  on_a_roll, comeback predicates (previously only covered via check_achievements())
- 141 core tests now passing

P2 — server tests:
- solitaire_server/src/sync.rs: 4 unit tests for merge logic (no DB required)
- solitaire_server/src/leaderboard.rs: 2 unit tests for entry shape and sort order

P3 — documentation:
- Add struct-level ///  to 12 Plugin structs (ChallengePlugin, CursorPlugin,
  AnimationPlugin, HelpPlugin, PausePlugin, AudioPlugin, DailyChallengePlugin,
  HudPlugin, LeaderboardPlugin, OnboardingPlugin, TimeAttackPlugin, WeeklyGoalsPlugin)
- Add field-level /// to Card, Pile, Deck, GameState, AchievementContext, AchievementDef
- Add /// to WeeklyGoalKind, WeeklyGoalDef, WeeklyGoalContext, StatsExt::update_on_win

card_animation module (new files from previous session):
- chain.rs, diagnostics.rs, tuning.rs, updated interaction.rs/animation.rs/mod.rs/lib.rs
- Remove unused HOVER_SCALE_DEFAULT / DRAG_LIFT_SCALE_DEFAULT / HOVER_LERP_SPEED_DEFAULT constants
- Add handle_touch_stock_tap so touch users can draw from the stock pile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:02:52 +00:00
funman300 59a023ed5e chore(workspace): fix all clippy warnings in test code
Resolves 15 violations found by `cargo clippy --workspace --tests -D warnings`:
- Remove unused imports (Card, Rank) in cursor_plugin tests
- Replace absurd i32::MAX comparison with a meaningful >= 0 check
- Use range .contains() instead of manual >= && <= (manual_range_contains)
- Move impl FromRequestParts before test module in middleware.rs (items_after_test_module)
- Move _VEC3_REFERENCED const before test module in input_plugin.rs
- Convert runtime assert on constant to const { assert!(...) }
- Use .contains() instead of .iter().any() for slice membership
- Replace .get(...).is_none() with !.contains_key(...) in HashMap checks
- Collapse Default::default() + field assignment into struct literal initializers
  across solitaire_sync, solitaire_data, and solitaire_engine test helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:02:27 +00:00
funman300 f32e53dd0b feat(engine): shake/settle/deal animations (#54, #55, #69)
Add FeedbackAnimPlugin with three card feedback animations:
- #54 ShakeAnim: horizontal shake on MoveRejectedEvent targeting
  destination pile cards; 0.3 s damped sine wave
- #55 SettleAnim: Y-scale bounce on valid placement (StateChangedEvent);
  1.0 → 0.92 → 1.0 over 0.15 s for all top-of-pile cards
- #69 Deal animation: slides each card from stock position to its deal
  position on NewGameRequestEvent (move_count == 0), using existing
  CardAnim with 0.04 s per-card stagger

Pure-function helpers shake_offset, settle_scale, and deal_stagger_delay
are public and covered by 6 unit tests. Fix pre-existing compile/clippy
errors: stubbed handle_confirm_input/handle_game_over_input, removed dead
CycleCardBack/CycleBackground variants, annotated ambient_handle field,
and fixed draw_mode.clone() in pause_plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:55:24 +00:00
funman300 ddd7502a06 feat(engine): playability improvements — input intelligence, audio, HUD, onboarding (#27–#30, #37, #39–#40, #44, #48–#49)
Task #27: Double-click auto-move — best_destination() finds optimal target
(foundation over tableau); handle_double_click() fires MoveRequestEvent.

Task #28: Hint system — find_hint() returns first legal from/to/count triple;
H key tints the source stack HintHighlight (yellow pulse via tick_hint_highlight).

Task #29: No-moves detection — has_legal_moves() checks stock/waste/all face-up
cards; check_no_moves system fires InfoToastEvent("No moves available") once per
stalemate (debounced so it fires only once until the state changes).

Task #30: Forfeit — G key fires ForfeitEvent; StatsPlugin records abandoned game,
persists stats, starts a new deal.

Task #37: Mute-all (M) and mute-music (Shift+M) toggles; MuteState resource
applied in apply_volume_on_change.

Task #39: Daily challenge HUD constraint label (time limit / target score).

Task #40: Undo-count HUD label; amber colour when undos > 0.

Task #44: Win-streak and level line on pause screen.

Task #48: Undo sound routes UndoRequestEvent → lib.flip audio channel.

Task #49: Onboarding banner rich-text key highlights — D and H rendered as
orange KeyHighlightSpan children so they stand out from body text.

Also registers CursorPlugin in solitaire_app (tasks #31/#32 wire-up).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:11:47 +00:00
funman300 c3ee7c45a7 feat(engine): card visual improvements — flip animation, foundation/tableau placeholders, drag shadow
Task #34: CardFlipAnim component + start_flip_anim/tick_flip_anim systems animate revealed
cards by squashing scale.x to 0 then expanding back to 1 (2×0.08 s). Skipped at Instant speed.

Task #35: spawn_pile_markers now adds a Text2d child (S/H/D/C, 45% alpha) on Foundation
markers so the suit is visible while the pile is empty.

Task #43: Tableau pile markers get a "K" Text2d child (35% alpha) indicating only Kings land
on empty columns.

Task #38: update_drag_shadow system maintains a single ShadowEntity while dragging — a
card_w+8 × card_h+8 dark semi-transparent sprite at z−1 behind the top dragged card.

Also fixed pre-existing clippy/compiler errors in hud_plugin, pause_plugin, stats_plugin,
cursor_plugin, and settings_plugin (missing imports, too-many-arguments, doc formatting).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:03:59 +00:00
root bc021acfd0 feat(data,engine): add WinUnder-5min and WinDrawThree weekly goal types
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>
2026-04-27 03:13:33 +00:00
root ed0aff4714 feat(data): expand challenge seed list from 5 to 25 seeds
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>
2026-04-27 02:42:50 +00:00
root 314186d6f4 feat(data): SyncProvider::delete_account + SolitaireServerClient impl
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>
2026-04-27 02:10:26 +00:00
root 648c5c18d9 feat(leaderboard): opt-out support — server endpoint, client method, UI button
- Server: DELETE /api/leaderboard/opt-in sets leaderboard_opt_in=0,
  hiding the player without deleting their row (scores preserved for re-opt-in)
- SyncProvider trait: opt_out_leaderboard() default no-op method + blanket impl
- SolitaireServerClient: implements opt_out_leaderboard via DELETE request with JWT refresh
- Leaderboard UI: "Opt Out" button (dark red) alongside existing "Opt In" button
- Server integration test: opt-out hides, opt-in restores (round-trip verified)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 02:01:20 +00:00
root f579b96d76 feat(engine): wire AnimSpeed to animation, new achievements, leaderboard opt-in, daily goal display
- AnimSpeed setting now drives card slide duration (Normal=0.15s, Fast=0.07s, Instant=snap);
  EffectiveSlideDuration resource updated on SettingsChangedEvent; AnimSpeed row added to Settings panel
- GameState.recycle_count tracks waste recycles; perfectionist/comeback/zen_winner achievements added
  with full unit tests
- SyncProvider gains opt_in_leaderboard(); SolitaireServerClient implements POST /api/leaderboard/opt-in;
  Opt In button added to leaderboard panel
- DailyChallengeResource stores goal_description/target_score/max_time_secs from server;
  pressing C shows goal description as toast (DailyGoalAnnouncementEvent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 01:38:25 +00:00
root 9a38873891 feat(engine): fetch daily-challenge seed from server on startup
- 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>
2026-04-27 01:09:24 +00:00
root 9a4071c74e refactor(engine): propagate SyncError through pull task instead of String
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>
2026-04-27 01:05:59 +00:00
root 6728a4311f feat(engine): grant achievement rewards + gate cosmetic selectors
- 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>
2026-04-27 01:00:18 +00:00
root d56abcd7a9 feat(engine): leaderboard screen (press L to toggle)
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>
2026-04-27 00:38:55 +00:00
root 00f0383867 feat(engine): in-progress game state persistence
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>
2026-04-27 00:17:47 +00:00
root 34ba4dc6ed feat(workspace): full server + sync implementation, all tests green
- 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>
2026-04-26 23:32:56 +00:00
funman300 9d0f9478b2 feat(data,engine): persistent Settings + SFX volume hotkeys
- 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>
2026-04-25 23:08:20 -07:00
funman300 788ac9f65a feat(engine,core,data): add Challenge mode with seed list and level-5 gate
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>
2026-04-25 17:18:32 -07:00
funman300 b730902d76 feat(engine): add weekly goals with ISO-week rollover and +75 XP bonus
Phase 6 part 2b:
- solitaire_data::weekly defines WeeklyGoalKind, WeeklyGoalDef,
  WeeklyGoalContext, current_iso_week_key, and three starter goals
  (5 wins, 3 no-undo wins, 3 fast wins).
- PlayerProgress gains weekly_goal_week_iso, roll_weekly_goals_if_new_week,
  and record_weekly_progress (returns true exactly once per goal completion).
- WeeklyGoalsPlugin evaluates GameWonEvent against WEEKLY_GOALS, rolls the
  week if needed, increments matching counters, awards WEEKLY_GOAL_XP for
  newly-completed goals, persists progress, and fires
  WeeklyGoalCompletedEvent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:25:18 -07:00
funman300 622b35a3bf feat(engine): add daily challenge, level-up toast, and daily_devotee achievement
Phase 6 part 2 (partial):
- daily_seed_for(date) and PlayerProgress::record_daily_completion in
  solitaire_data, with streak logic that increments on consecutive days,
  resets on a skipped day, and is idempotent on same-day re-completions.
- DailyChallengePlugin tracks today's seed, awards +100 XP and updates
  the streak when the player wins a game whose seed matches. Pressing C
  starts a new game with the daily seed.
- LevelUpEvent toast in AnimationPlugin announces level changes.
- AchievementContext gains daily_challenge_streak; daily_devotee
  achievement unlocks at streak >= 7. AchievementPlugin reads
  ProgressResource and runs after ProgressUpdate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:17:59 -07:00
funman300 cfdb3b7547 feat(data): add PlayerProgress with XP/level helpers and atomic persistence
level_for_xp implements the two-segment level formula from
ARCHITECTURE.md §13. xp_for_win = base 50 + linearly-scaled speed bonus
(10..=50 for sub-2-minute wins) + 25 if no undo was used. PlayerProgress
exposes add_xp returning the previous level so callers can detect
level-up events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:10:28 -07:00
funman300 4589c52368 feat(data): add AchievementRecord and atomic achievements.json persistence
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 12:51:15 -07:00
funman300 5ced4c01ce feat(data): add atomic stats persistence (load_stats_from, save_stats_to)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 12:37:57 -07:00
funman300 f8cce2433d feat(data): add StatsSnapshot with update_on_win and record_abandoned
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 12:37:21 -07:00
Solitaire Quest f84d7c5849 fix(workspace): add derives/docs per code review, remove unused thiserror from solitaire_sync 2026-04-23 11:04:15 -07:00
Solitaire Quest 684f07746d feat(workspace): initialize all seven crates with stubs and blank Bevy window
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 11:00:42 -07:00