feat(difficulty): add difficulty-tier game mode with seed catalogs and home UI

Adds DifficultyLevel (Easy/Medium/Hard/Expert/Grandmaster/Random) to
solitaire_core::game_state alongside GameMode::Difficulty(DifficultyLevel).
Five seed catalogs (40 seeds each) are pre-verified by the new
gen_difficulty_seeds binary using tiered solver budgets (1K–200K moves).
DifficultyPlugin resolves StartDifficultyRequestEvent → catalog seed →
NewGameRequestEvent; Random uses a system-time seed and bypasses the
winnable-only filter. The home overlay gets an expandable Difficulty section
between Draw Mode and the mode grid; last-played tier persists in Settings.
Difficulty wins pool into Classic stats. 5 unit tests in difficulty_plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-08 21:07:49 -07:00
parent 4df962ee07
commit 4303ef3f5b
14 changed files with 1074 additions and 29 deletions
+9 -1
View File
@@ -9,7 +9,7 @@ use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use solitaire_core::game_state::DrawMode;
use solitaire_core::game_state::{DifficultyLevel, DrawMode};
const APP_DIR_NAME: &str = "solitaire_quest";
const SETTINGS_FILE_NAME: &str = "settings.json";
@@ -224,6 +224,13 @@ pub struct Settings {
/// `#[serde(default = "default_replay_move_interval_secs")]`.
#[serde(default = "default_replay_move_interval_secs")]
pub replay_move_interval_secs: f32,
/// Last difficulty tier the player selected. `None` means the player has
/// never used the difficulty picker. When `Some`, the difficulty section in
/// the home overlay opens pre-expanded and highlights this tier. Older
/// `settings.json` files written before this field existed deserialize
/// cleanly to `None` via `#[serde(default)]`.
#[serde(default)]
pub last_difficulty: Option<DifficultyLevel>,
}
fn default_draw_mode() -> DrawMode {
@@ -342,6 +349,7 @@ impl Default for Settings {
winnable_deals_only: false,
disable_smart_default_size: false,
replay_move_interval_secs: default_replay_move_interval_secs(),
last_difficulty: None,
}
}
}