4303ef3f5b
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>
321 lines
9.5 KiB
Rust
321 lines
9.5 KiB
Rust
//! Pre-verified seed catalogs for each [`DifficultyLevel`] tier.
|
|
//!
|
|
//! Each slice contains seeds that are provably winnable in Draw-One mode and
|
|
//! that required a specific solver-budget range to solve — the **smallest**
|
|
//! budget that returns `Winnable` determines the tier. See
|
|
//! `solitaire_assetgen/src/bin/gen_difficulty_seeds.rs` for the generator.
|
|
//!
|
|
//! # Tiers and budget boundaries
|
|
//!
|
|
//! | Tier | move_budget | state_budget |
|
|
//! |-------------|-------------|--------------|
|
|
//! | Easy | 1 000 | 1 000 |
|
|
//! | Medium | 5 000 | 5 000 |
|
|
//! | Hard | 25 000 | 25 000 |
|
|
//! | Expert | 100 000 | 100 000 |
|
|
//! | Grandmaster | 200 000 | 200 000 |
|
|
//!
|
|
//! [`DifficultyLevel::Random`] has no catalog — the engine picks a system-time
|
|
//! seed and skips verification.
|
|
|
|
use solitaire_core::game_state::DifficultyLevel;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Catalogs (populated by gen_difficulty_seeds)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// 40 seeds proven winnable within the Easy budget (≤ 1 000 states).
|
|
pub const EASY_SEEDS: &[u64] = &[
|
|
// Generated by solitaire_assetgen::gen_difficulty_seeds (tier=Easy, date=2026-05-09)
|
|
0xD1FF_0000_0000_0001,
|
|
0xD1FF_0000_0000_0002,
|
|
0xD1FF_0000_0000_0007,
|
|
0xD1FF_0000_0000_0008,
|
|
0xD1FF_0000_0000_0009,
|
|
0xD1FF_0000_0000_000E,
|
|
0xD1FF_0000_0000_0013,
|
|
0xD1FF_0000_0000_0015,
|
|
0xD1FF_0000_0000_0018,
|
|
0xD1FF_0000_0000_001D,
|
|
0xD1FF_0000_0000_0021,
|
|
0xD1FF_0000_0000_0022,
|
|
0xD1FF_0000_0000_0026,
|
|
0xD1FF_0000_0000_002C,
|
|
0xD1FF_0000_0000_002E,
|
|
0xD1FF_0000_0000_002F,
|
|
0xD1FF_0000_0000_0035,
|
|
0xD1FF_0000_0000_0036,
|
|
0xD1FF_0000_0000_003C,
|
|
0xD1FF_0000_0000_0045,
|
|
0xD1FF_0000_0000_0046,
|
|
0xD1FF_0000_0000_0048,
|
|
0xD1FF_0000_0000_0049,
|
|
0xD1FF_0000_0000_004D,
|
|
0xD1FF_0000_0000_004F,
|
|
0xD1FF_0000_0000_0050,
|
|
0xD1FF_0000_0000_0051,
|
|
0xD1FF_0000_0000_0053,
|
|
0xD1FF_0000_0000_0054,
|
|
0xD1FF_0000_0000_0057,
|
|
0xD1FF_0000_0000_0058,
|
|
0xD1FF_0000_0000_005A,
|
|
0xD1FF_0000_0000_005B,
|
|
0xD1FF_0000_0000_005C,
|
|
0xD1FF_0000_0000_005D,
|
|
0xD1FF_0000_0000_005F,
|
|
0xD1FF_0000_0000_0061,
|
|
0xD1FF_0000_0000_0062,
|
|
0xD1FF_0000_0000_0063,
|
|
0xD1FF_0000_0000_0069,
|
|
];
|
|
|
|
/// 40 seeds proven winnable within the Medium budget (≤ 5 000 states).
|
|
pub const MEDIUM_SEEDS: &[u64] = &[
|
|
// Generated by solitaire_assetgen::gen_difficulty_seeds (tier=Medium, date=2026-05-09)
|
|
0xD1FF_0000_0000_0000,
|
|
0xD1FF_0000_0000_0012,
|
|
0xD1FF_0000_0000_0016,
|
|
0xD1FF_0000_0000_001B,
|
|
0xD1FF_0000_0000_001C,
|
|
0xD1FF_0000_0000_0020,
|
|
0xD1FF_0000_0000_002A,
|
|
0xD1FF_0000_0000_0034,
|
|
0xD1FF_0000_0000_003A,
|
|
0xD1FF_0000_0000_0041,
|
|
0xD1FF_0000_0000_0043,
|
|
0xD1FF_0000_0000_0060,
|
|
0xD1FF_0000_0000_006A,
|
|
0xD1FF_0000_0000_006C,
|
|
0xD1FF_0000_0000_006E,
|
|
0xD1FF_0000_0000_006F,
|
|
0xD1FF_0000_0000_0071,
|
|
0xD1FF_0000_0000_0072,
|
|
0xD1FF_0000_0000_0075,
|
|
0xD1FF_0000_0000_0076,
|
|
0xD1FF_0000_0000_007B,
|
|
0xD1FF_0000_0000_007E,
|
|
0xD1FF_0000_0000_0081,
|
|
0xD1FF_0000_0000_0083,
|
|
0xD1FF_0000_0000_0084,
|
|
0xD1FF_0000_0000_0087,
|
|
0xD1FF_0000_0000_0090,
|
|
0xD1FF_0000_0000_0092,
|
|
0xD1FF_0000_0000_0093,
|
|
0xD1FF_0000_0000_0098,
|
|
0xD1FF_0000_0000_0099,
|
|
0xD1FF_0000_0000_009A,
|
|
0xD1FF_0000_0000_009E,
|
|
0xD1FF_0000_0000_00A5,
|
|
0xD1FF_0000_0000_00A8,
|
|
0xD1FF_0000_0000_00AA,
|
|
0xD1FF_0000_0000_00AB,
|
|
0xD1FF_0000_0000_00AE,
|
|
0xD1FF_0000_0000_00AF,
|
|
0xD1FF_0000_0000_00B0,
|
|
];
|
|
|
|
/// 40 seeds proven winnable within the Hard budget (≤ 25 000 states).
|
|
pub const HARD_SEEDS: &[u64] = &[
|
|
// Generated by solitaire_assetgen::gen_difficulty_seeds (tier=Hard, date=2026-05-09)
|
|
0xD1FF_0000_0000_001F,
|
|
0xD1FF_0000_0000_0024,
|
|
0xD1FF_0000_0000_0025,
|
|
0xD1FF_0000_0000_0031,
|
|
0xD1FF_0000_0000_0032,
|
|
0xD1FF_0000_0000_003E,
|
|
0xD1FF_0000_0000_004A,
|
|
0xD1FF_0000_0000_006D,
|
|
0xD1FF_0000_0000_0079,
|
|
0xD1FF_0000_0000_007C,
|
|
0xD1FF_0000_0000_0080,
|
|
0xD1FF_0000_0000_008A,
|
|
0xD1FF_0000_0000_0097,
|
|
0xD1FF_0000_0000_00B1,
|
|
0xD1FF_0000_0000_00B2,
|
|
0xD1FF_0000_0000_00B3,
|
|
0xD1FF_0000_0000_00B5,
|
|
0xD1FF_0000_0000_00B7,
|
|
0xD1FF_0000_0000_00B8,
|
|
0xD1FF_0000_0000_00B9,
|
|
0xD1FF_0000_0000_00BA,
|
|
0xD1FF_0000_0000_00BB,
|
|
0xD1FF_0000_0000_00BC,
|
|
0xD1FF_0000_0000_00BD,
|
|
0xD1FF_0000_0000_00C2,
|
|
0xD1FF_0000_0000_00C3,
|
|
0xD1FF_0000_0000_00C5,
|
|
0xD1FF_0000_0000_00CC,
|
|
0xD1FF_0000_0000_00CE,
|
|
0xD1FF_0000_0000_00D1,
|
|
0xD1FF_0000_0000_00D2,
|
|
0xD1FF_0000_0000_00D6,
|
|
0xD1FF_0000_0000_00D7,
|
|
0xD1FF_0000_0000_00DC,
|
|
0xD1FF_0000_0000_00DF,
|
|
0xD1FF_0000_0000_00E0,
|
|
0xD1FF_0000_0000_00E1,
|
|
0xD1FF_0000_0000_00E4,
|
|
0xD1FF_0000_0000_00E6,
|
|
0xD1FF_0000_0000_00E7,
|
|
];
|
|
|
|
/// 40 seeds proven winnable within the Expert budget (≤ 100 000 states).
|
|
pub const EXPERT_SEEDS: &[u64] = &[
|
|
// Generated by solitaire_assetgen::gen_difficulty_seeds (tier=Expert, date=2026-05-09)
|
|
0xD1FF_0000_0000_0006,
|
|
0xD1FF_0000_0000_000B,
|
|
0xD1FF_0000_0000_0019,
|
|
0xD1FF_0000_0000_0082,
|
|
0xD1FF_0000_0000_00CB,
|
|
0xD1FF_0000_0000_00D5,
|
|
0xD1FF_0000_0000_00D8,
|
|
0xD1FF_0000_0000_00E8,
|
|
0xD1FF_0000_0000_00EA,
|
|
0xD1FF_0000_0000_00EB,
|
|
0xD1FF_0000_0000_00EC,
|
|
0xD1FF_0000_0000_00ED,
|
|
0xD1FF_0000_0000_00F2,
|
|
0xD1FF_0000_0000_00F3,
|
|
0xD1FF_0000_0000_00F4,
|
|
0xD1FF_0000_0000_00FE,
|
|
0xD1FF_0000_0000_00FF,
|
|
0xD1FF_0000_0000_0102,
|
|
0xD1FF_0000_0000_0103,
|
|
0xD1FF_0000_0000_0104,
|
|
0xD1FF_0000_0000_0105,
|
|
0xD1FF_0000_0000_0106,
|
|
0xD1FF_0000_0000_0109,
|
|
0xD1FF_0000_0000_010B,
|
|
0xD1FF_0000_0000_010C,
|
|
0xD1FF_0000_0000_0110,
|
|
0xD1FF_0000_0000_0113,
|
|
0xD1FF_0000_0000_0114,
|
|
0xD1FF_0000_0000_011B,
|
|
0xD1FF_0000_0000_011C,
|
|
0xD1FF_0000_0000_011E,
|
|
0xD1FF_0000_0000_0120,
|
|
0xD1FF_0000_0000_0121,
|
|
0xD1FF_0000_0000_0122,
|
|
0xD1FF_0000_0000_0123,
|
|
0xD1FF_0000_0000_0124,
|
|
0xD1FF_0000_0000_0126,
|
|
0xD1FF_0000_0000_012B,
|
|
0xD1FF_0000_0000_012C,
|
|
0xD1FF_0000_0000_012E,
|
|
];
|
|
|
|
/// 40 seeds proven winnable only within the Grandmaster budget (≤ 200 000 states).
|
|
pub const GRANDMASTER_SEEDS: &[u64] = &[
|
|
// Generated by solitaire_assetgen::gen_difficulty_seeds (tier=Grandmaster, date=2026-05-09)
|
|
0xD1FF_0000_0000_0027,
|
|
0xD1FF_0000_0000_00A0,
|
|
0xD1FF_0000_0000_00C4,
|
|
0xD1FF_0000_0000_00D4,
|
|
0xD1FF_0000_0000_00DE,
|
|
0xD1FF_0000_0000_00F9,
|
|
0xD1FF_0000_0000_0107,
|
|
0xD1FF_0000_0000_0108,
|
|
0xD1FF_0000_0000_0130,
|
|
0xD1FF_0000_0000_0132,
|
|
0xD1FF_0000_0000_0133,
|
|
0xD1FF_0000_0000_0134,
|
|
0xD1FF_0000_0000_0135,
|
|
0xD1FF_0000_0000_0137,
|
|
0xD1FF_0000_0000_0139,
|
|
0xD1FF_0000_0000_013A,
|
|
0xD1FF_0000_0000_013D,
|
|
0xD1FF_0000_0000_013F,
|
|
0xD1FF_0000_0000_0140,
|
|
0xD1FF_0000_0000_0141,
|
|
0xD1FF_0000_0000_0142,
|
|
0xD1FF_0000_0000_0143,
|
|
0xD1FF_0000_0000_0145,
|
|
0xD1FF_0000_0000_0146,
|
|
0xD1FF_0000_0000_014A,
|
|
0xD1FF_0000_0000_014B,
|
|
0xD1FF_0000_0000_014C,
|
|
0xD1FF_0000_0000_014D,
|
|
0xD1FF_0000_0000_014F,
|
|
0xD1FF_0000_0000_0150,
|
|
0xD1FF_0000_0000_0151,
|
|
0xD1FF_0000_0000_0152,
|
|
0xD1FF_0000_0000_0153,
|
|
0xD1FF_0000_0000_0157,
|
|
0xD1FF_0000_0000_0158,
|
|
0xD1FF_0000_0000_015B,
|
|
0xD1FF_0000_0000_015C,
|
|
0xD1FF_0000_0000_015E,
|
|
0xD1FF_0000_0000_0162,
|
|
0xD1FF_0000_0000_0164,
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Type alias for the catalog lookup return: a static slice or `None` for `Random`.
|
|
pub type DifficultySeeds = Option<&'static [u64]>;
|
|
|
|
/// Return the seed catalog for `level`, or `None` for `Random` (caller must
|
|
/// use a system-time seed instead).
|
|
pub fn seeds_for(level: DifficultyLevel) -> DifficultySeeds {
|
|
match level {
|
|
DifficultyLevel::Easy => Some(EASY_SEEDS),
|
|
DifficultyLevel::Medium => Some(MEDIUM_SEEDS),
|
|
DifficultyLevel::Hard => Some(HARD_SEEDS),
|
|
DifficultyLevel::Expert => Some(EXPERT_SEEDS),
|
|
DifficultyLevel::Grandmaster => Some(GRANDMASTER_SEEDS),
|
|
DifficultyLevel::Random => None,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn all_difficulty_seeds_are_unique() {
|
|
let all: Vec<u64> = [
|
|
EASY_SEEDS,
|
|
MEDIUM_SEEDS,
|
|
HARD_SEEDS,
|
|
EXPERT_SEEDS,
|
|
GRANDMASTER_SEEDS,
|
|
]
|
|
.iter()
|
|
.flat_map(|s| s.iter().copied())
|
|
.collect();
|
|
|
|
let mut sorted = all.clone();
|
|
sorted.sort_unstable();
|
|
let before = sorted.len();
|
|
sorted.dedup();
|
|
assert_eq!(sorted.len(), before, "duplicate seeds found across difficulty tiers");
|
|
}
|
|
|
|
#[test]
|
|
fn seeds_for_random_returns_none() {
|
|
assert!(seeds_for(DifficultyLevel::Random).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn seeds_for_non_random_returns_some() {
|
|
for level in [
|
|
DifficultyLevel::Easy,
|
|
DifficultyLevel::Medium,
|
|
DifficultyLevel::Hard,
|
|
DifficultyLevel::Expert,
|
|
DifficultyLevel::Grandmaster,
|
|
] {
|
|
assert!(
|
|
seeds_for(level).is_some(),
|
|
"{level:?} should return Some catalog"
|
|
);
|
|
}
|
|
}
|
|
}
|