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>
This commit is contained in:
root
2026-04-27 00:17:47 +00:00
parent 20db4b312a
commit 00f0383867
6 changed files with 303 additions and 10 deletions
+20
View File
@@ -9,6 +9,24 @@ use crate::scoring::{compute_time_bonus as scoring_time_bonus, score_move, score
const MAX_UNDO_STACK: usize = 64;
/// Serialize `HashMap<PileType, Pile>` as a `Vec` of `(key, value)` pairs so
/// that JSON (which requires string map keys) round-trips correctly.
mod pile_map_serde {
use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::pile::{Pile, PileType};
pub fn serialize<S: Serializer>(map: &HashMap<PileType, Pile>, s: S) -> Result<S::Ok, S::Error> {
let entries: Vec<(&PileType, &Pile)> = map.iter().collect();
entries.serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<HashMap<PileType, Pile>, D::Error> {
let entries: Vec<(PileType, Pile)> = Vec::deserialize(d)?;
Ok(entries.into_iter().collect())
}
}
/// Whether cards are drawn one at a time or three at a time from the stock.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DrawMode {
@@ -37,6 +55,7 @@ pub enum GameMode {
/// Snapshot of game state used for undo.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct StateSnapshot {
#[serde(with = "pile_map_serde")]
piles: HashMap<PileType, Pile>,
score: i32,
move_count: u32,
@@ -45,6 +64,7 @@ struct StateSnapshot {
/// Full state of an in-progress Klondike Solitaire game.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GameState {
#[serde(with = "pile_map_serde")]
pub piles: HashMap<PileType, Pile>,
pub draw_mode: DrawMode,
/// Top-level mode (Classic / Zen). Defaults to Classic for backwards