Files
Ferrous-Solitaire/docs/SESSION_HANDOFF.md
T
2026-04-24 19:25:31 -07:00

10 KiB
Raw Blame History

Solitaire Quest — Session Handoff

Last updated: 2026-04-24 Branch: master — pushed to https://git.aleshym.co/funman300/Rusty_Solitare.git Test count: 196 passing (77 core + 50 data + 69 engine), cargo clippy --workspace -- -D warnings clean


What Has Been Built

Phase 1 — Workspace Setup COMPLETE

All seven Cargo crates created and compiling cleanly:

Crate Status Purpose
solitaire_core Fully implemented Pure Rust game logic — NO Bevy, NO network
solitaire_sync Stub Shared API types (SyncPayload, SyncResponse)
solitaire_data Stub SyncError enum + SyncProvider trait
solitaire_engine Stub Bevy ECS systems — all plugins added in Phase 3
solitaire_server Stub Axum sync server — implemented in Phase 8C
solitaire_gpgs Compile-time stub Google Play Games bridge — Android only, JNI in Phase: Android
solitaire_app Working Opens blank Bevy window titled "Solitaire Quest" at 1280×800

Fast compile profiles, assets/ directory structure, and .env.example are all in place.

Phase 2 — Core Game Engine COMPLETE

solitaire_core is fully implemented with 68 passing tests and zero clippy warnings.

Modules:

  • card.rsSuit (Clubs/Diamonds/Hearts/Spades, is_red()/is_black()), Rank (AceKing, value() -> u8), Card (id, suit, rank, face_up)
  • pile.rsPileType (Stock, Waste, Foundation(Suit), Tableau(usize)), Pile (new, top)
  • error.rsMoveError: InvalidSource, InvalidDestination, EmptySource, RuleViolation(String), UndoStackEmpty, GameAlreadyWon, StockEmpty
  • deck.rsDeck::new(), Deck::shuffle(seed: u64) using seeded StdRng (cross-platform deterministic), deal_klondike(deck) -> ([Pile; 7], Pile)
  • rules.rscan_place_on_foundation(card, pile, suit), can_place_on_tableau(card, pile)
  • scoring.rsscore_move(from, to), score_undo() (-15), compute_time_bonus(elapsed_seconds) (700_000/s)
  • game_state.rsDrawMode, GameState with full game loop

GameState public API:

GameState::new(seed: u64, draw_mode: DrawMode) -> Self
GameState::draw(&mut self) -> Result<(), MoveError>
GameState::move_cards(&mut self, from: PileType, to: PileType, count: usize) -> Result<(), MoveError>
GameState::undo(&mut self) -> Result<(), MoveError>
GameState::check_win(&self) -> bool
GameState::check_auto_complete(&self) -> bool
GameState::compute_time_bonus(&self) -> i32
GameState::undo_stack_len(&self) -> usize

Key GameState rules:

  • Undo stack capped at 64 entries (oldest evicted)
  • Score never goes below 0
  • Waste recycling is unlimited — StockEmpty only when both stock AND waste are simultaneously empty
  • Recycle (waste → stock) pushes a snapshot so it can be undone
  • Newly exposed top card of source pile is flipped face-up automatically on move_cards
  • Win: all 4 foundations at 13 cards
  • Auto-complete: stock empty + waste empty + all tableau cards face-up

Commit History

b8dc7cb fix(core): remove stock_recycled limit, replace unwrap, snapshot on recycle, fix derives
58f1465 feat(core): add GameState with draw, move_cards, undo, win/auto-complete detection
43194b0 fix(core): use StdRng doc comment, replace expect() with debug_assert in deal_klondike
17bbec0 feat(core): add pile, error, deck, rules, scoring modules with tests
fcf878b feat(core): add Card, Suit, Rank types with tests
f84d7c5 fix(workspace): add derives/docs per code review, remove unused thiserror from solitaire_sync
684f077 feat(workspace): initialize all seven crates with stubs and blank Bevy window

Phase 3 — Bevy Rendering & Interaction COMPLETE

All sub-phases (3A3F) done. Plugins: GamePlugin, TablePlugin, CardPlugin, InputPlugin, AnimationPlugin. Full game playable — drag/drop with rule validation, keyboard shortcuts (U/N/D/Esc), animated slides, win cascade. UI via bevy::ui, no egui.

Phase 4 — Statistics Persistence COMPLETE

  • solitaire_data::StatsSnapshot with update_on_win / record_abandoned / win_rate
  • Atomic file I/O via save_stats_to (.tmp → rename)
  • StatsPlugin in solitaire_engine — loads on startup, persists on GameWonEvent (win) and NewGameRequestEvent (abandoned if move_count>0 and not won)
  • Full-window overlay toggled with S — games played/won, win rate, streak, best score, fastest, avg
  • StatsPlugin::default() for production, StatsPlugin::headless() for tests (no disk I/O)

Phase 5 — Achievements COMPLETE (14 of ~19)

  • solitaire_core::achievementAchievementContext + AchievementDef + ALL_ACHIEVEMENTS + check_achievements
  • solitaire_core::GameState.undo_count — tracks whether undo was used (for no_undo / speed_and_skill)
  • solitaire_data::AchievementRecord + atomic achievements.json persistence
  • AchievementPlugin — on GameWonEvent, build context from StatsResource + GameState + chrono::Local hour, evaluate all conditions, persist newly-unlocked records, emit AchievementUnlockedEvent(id)
  • AnimationPlugin's toast resolves the event's ID to the achievement's name via achievement_plugin::display_name_for
  • New StatsUpdate system set lets AchievementPlugin order itself after stats are incremented
  • Deferred: daily_devotee (needs PlayerProgress), comeback (needs recycle counter), zen_winner (needs modes), perfectionist (needs max-score calc). Stubs can be added in later phases.

Phase 6 (part 1) — XP, Levels, ProgressPlugin COMPLETE

  • solitaire_data::PlayerProgress with total_xp, level, daily/weekly/unlock fields
  • level_for_xp(xp) and xp_for_win(time, used_undo) helpers (per ARCHITECTURE.md §13)
  • add_xp(amount) -> prev_level with leveled_up_from(prev) for level-up detection
  • Atomic progress.json persistence via save_progress_to / load_progress_from
  • ProgressPlugin — on GameWonEvent, awards XP (base 50 + speed bonus 1050 + no-undo 25), persists, emits LevelUpEvent
  • ProgressUpdate system set for ordering downstream systems
  • ProgressPlugin::default() for production, ::headless() for tests

Phase 6 (part 2a) — Daily Challenge + Level-Up Toast COMPLETE

  • daily_seed_for(date) deterministic per-date seed
  • PlayerProgress::record_daily_completion(date) with streak / reset / idempotency rules
  • DailyChallengePlugin: today's seed in a resource; pressing C starts a daily-seed new game; on winning a daily-seed game, awards +100 XP, updates streak, persists, fires DailyChallengeCompletedEvent
  • LevelUpEvent now spawns a toast through AnimationPlugin
  • daily_devotee achievement wired (streak ≥ 7); AchievementContext gains daily_challenge_streak and reads from ProgressResource

Phase 6 (part 2b) — Weekly Goals COMPLETE

  • solitaire_data::weeklyWeeklyGoalKind, WeeklyGoalDef, WeeklyGoalContext, current_iso_week_key, three starter goals (5 wins / 3 no-undo / 3 fast)
  • PlayerProgressweekly_goal_week_iso, roll_weekly_goals_if_new_week, record_weekly_progress
  • WeeklyGoalsPlugin — on GameWonEvent, rolls week if needed, increments matching goals, awards WEEKLY_GOAL_XP (75) per completion, fires WeeklyGoalCompletedEvent

What Is Next

Phase 6 (part 3) — Special Modes + Status UI

  • Time Attack / Challenge / Zen modes — unlock at level 5. Need a GameMode enum (likely in solitaire_core) + mode selector + per-mode rule overrides.
  • Toast for DailyChallengeCompletedEvent and WeeklyGoalCompletedEvent (currently fire but not surfaced).
  • Daily/weekly status panel in the stats overlay (today's challenge state, weekly goal progress bars).
  • Card-back / background unlock UI for unlocked_card_backs / unlocked_backgrounds.

Phases 78 (in order after Phase 6 part 3)

Phase Scope
Phase 7 Audio (kira), polish, hints, onboarding, pause menu
Phase 8AC Local storage + SyncProvider + self-hosted Axum server + client
Phase 8D GPGS stub fully wired into settings UI

Important Implementation Notes

Versions (Cargo.toml workspace deps)

  • bevy = "0.15" (resolved to 0.15.3) — UI via built-in bevy::ui, no bevy_egui
  • kira = "0.9" — audio via kira crate directly, no bevy_kira_audio or AssetServer
  • rand = "0.8" — note: small_rng feature is NOT enabled; use StdRng, not SmallRng

Asset strategy

  • No AssetServer — assets embedded at compile time using include_bytes!()
  • Fonts: Font::try_from_bytes(include_bytes!("../assets/fonts/main.ttf"))
  • Audio: load from &[u8] via kira StaticSoundData::from_cursor()
  • Card rendering: procedural (bevy::prelude::Sprite + Text2d) — no sprite sheets required

Hard rules (from CLAUDE.md)

  • solitaire_core and solitaire_sync must NEVER gain Bevy or network dependencies
  • No unwrap() or panic!() in game logic — use Result<_, MoveError> everywhere
  • All state transitions return Resultdebug_assert! is acceptable for structural invariants
  • SyncPlugin must NEVER match on SyncBackend enum inside a Bevy system — always call through the SyncProvider trait
  • Atomic file writes only: write to .tmp then rename()
  • cargo clippy --workspace -- -D warnings must pass clean
  • cargo test --workspace must pass clean

Lessons from this session

  • rand = "0.8" without features = ["small_rng"] means SmallRng is unavailable — use StdRng
  • tower-governor uses underscores in the crate name (not hyphens in Cargo.toml)
  • When implementing draw() in GameState: recycle is unlimited, stop condition is BOTH piles empty simultaneously
  • Recycle must push a snapshot (so it can be undone) even though it doesn't count as a "move"

Implementation Plan Document

The detailed task-by-task plan for Phases 1 and 2 is at: docs/superpowers/plans/2026-04-20-phase1-2-workspace-core.md

For Phase 3 onwards, write a new plan using the superpowers:writing-plans skill before starting implementation.


Running the Project

# Check everything compiles
cargo check --workspace

# Run all tests (196 tests, all should pass)
cargo test --workspace

# Lint (must be zero warnings)
cargo clippy --workspace -- -D warnings

# Run the game
cargo run -p solitaire_app --features bevy/dynamic_linking