- 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>
- engine/lib.rs: re-export CardFaceRevealedEvent so external crates can consume flip-midpoint audio events
- gpgs/stub.rs: add explicit impls for all six defaulted SyncProvider methods; future trait changes now cause a compile error in the stub rather than silently picking up wrong defaults
- core/game_state.rs: add /// doc comments to DrawMode and GameMode variants
- server/auth.rs: replace terse BCRYPT_COST comment with full /// doc comment matching ARCHITECTURE.md §19
- server/leaderboard.rs: add /// doc comment to DISPLAY_NAME_MAX; fix misplaced comment that was prepended to the opt_in handler instead of the constant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SelectionPlugin: add clear_selection_on_state_change system so undo/move/reject
never leave a stale selection pointing at the wrong card
- SelectionPlugin: expose SelectionKeySet system set for cross-plugin ordering
- PausePlugin: skip Escape→pause when a card is keyboard-selected; toggle_pause
now runs before SelectionKeySet so it reads SelectionState before it is cleared
- InputPlugin: guard Space→DrawRequestEvent when SelectionState has an active pile
so Space executes a card move instead of also drawing from stock
- window: enforce 800×600 minimum via WindowResizeConstraints
- game_state: add precondition doc to next_auto_complete_move (waste exclusion)
- card_plugin: 12 unit tests for constants, face_colour, label_visibility, label_for
- pause_plugin: add paused_resource_default and draw_mode_label exhaustiveness tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
Three new tests: non-waste→tableau scores zero (tableau restack and
impossible foundation→tableau), move→stock/waste scores zero (guard
against non-obvious destinations panicking), and time_bonus capped at
i32::MAX via the .min() guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three tests: undo_count starts at zero, increments on each undo call,
and saturates at u32::MAX without panicking. The undo_count field is
read by ProgressPlugin to determine the no-undo XP bonus but had no
coverage in game_state tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three test cases were missing:
- waste_not_empty guard: stock clear, waste has a card, all tableau
face-up — must return false even with all tableau conditions satisfied
- true case: positive path confirming all-face-up + empty stock/waste
returns true (previously untested entirely)
- The previous face_down_cards test covered only that guard, not the
waste guard or the positive path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- King-on-Queen foundation completion (the end-game move had no test)
- King wrong-suit rejected on completed foundation
- Ace-on-Two valid placement (Ace.value()+1 == Two.value(), different color)
- Same-rank different-color invalid (rank difference is 0, not 1)
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>
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>
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>
- 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>
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>
- 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>
- 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>
Phase 6 part 4 (partial):
- GameState now tracks elapsed_seconds via tick_elapsed_time in GamePlugin
(per-second increment while not won). Pure helper advance_elapsed makes
the tick logic directly testable without mocking Bevy Time.
- New GameMode enum (Classic / Zen) on GameState. Zen mode suppresses
scoring in move_cards and undo. GameState::new_with_mode allows callers
to construct non-Classic games; the existing GameState::new still
defaults to Classic. mode is serde(default) for backwards-compatible
persistence.
- NewGameRequestEvent gains an optional mode field; handle_new_game
honours it (falling back to the current game's mode when None).
- InputPlugin: pressing Z starts a fresh Zen-mode game.
Time Attack, Challenge mode, level-5 unlock gating, and unlock UI are
still deferred.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Introduces AchievementContext (stats + last-win snapshot), AchievementDef,
ALL_ACHIEVEMENTS, and check_achievements. Adds undo_count to GameState
so the no_undo and speed_and_skill conditions are evaluable.
Skipped achievements that depend on features not yet built:
daily_devotee (progress), comeback (recycle counter), zen_winner (modes),
perfectionist (max-score calc). They land in later phases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove stock_recycled field: recycling is now unlimited; StockEmpty only when both stock and waste are empty
- Replace all .unwrap() in draw() and move_cards() with .ok_or(MoveError::...)? propagation
- Push snapshot before recycling waste→stock so undo can reverse it
- Make StateSnapshot private (struct, not pub struct) and undo_stack private (not pub(crate))
- Add PartialEq, Eq to derives on both StateSnapshot and GameState
- Update draw_from_empty_stock_and_waste_returns_error test to use direct pile clearing since unlimited recycling means the old loop-based approach never reached both-empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements PileType/Pile, MoveError (thiserror), Deck with seeded shuffle,
deal_klondike layout, foundation/tableau placement rules, and Windows XP
Standard scoring — 41 tests, clippy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>