use std::collections::HashMap; use std::sync::atomic::AtomicBool; use std::sync::Arc; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SymmetryType { Rotational180, Rotational90, Diagonal, Mirror, None, } impl Default for SymmetryType { fn default() -> Self { Self::Rotational180 } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DifficultyPreset { Glyph, Rune, Scroll, Codex, Grimoire, } impl DifficultyPreset { pub fn grid_size(&self) -> usize { match self { Self::Glyph => 5, Self::Rune => 5, Self::Scroll => 9, Self::Codex => 15, Self::Grimoire => 21, } } pub fn black_cell_density(&self) -> f64 { match self { Self::Glyph => 0.40, Self::Rune => 0.35, Self::Scroll => 0.28, Self::Codex => 0.20, Self::Grimoire => 0.18, } } pub fn frequency_threshold(&self) -> f64 { match self { Self::Glyph => 0.5, Self::Rune => 0.1, Self::Scroll => 0.0, Self::Codex => 0.0, Self::Grimoire => 0.0, } } pub fn difficulty_level(&self) -> u8 { match self { Self::Glyph => 0, Self::Rune => 1, Self::Scroll => 2, Self::Codex => 3, Self::Grimoire => 3, } } pub fn obscurity_budget(&self) -> f64 { match self { Self::Glyph => 0.0, Self::Rune => 0.0, Self::Scroll => 0.05, Self::Codex => 0.20, Self::Grimoire => 0.20, } } pub fn rand_top_base(&self) -> usize { match self { Self::Glyph => 20, Self::Rune => 20, Self::Scroll => 15, Self::Codex => 10, Self::Grimoire => 8, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenerationConfig { pub width: usize, pub height: usize, pub symmetry: SymmetryType, pub black_cell_density: f64, pub min_word_length: usize, pub frequency_threshold: f64, pub obscurity_budget: f64, pub max_backtrack: usize, pub max_restarts: usize, pub timeout_seconds: u64, pub seed: Option, pub cryptic_grid: bool, pub rand_top_base: usize, #[serde(skip)] pub cancelled: Option>, /// Per-word staleness penalties (0.0 = max penalty, 1.0 = no penalty). /// Applied during solver scoring to deprioritize recently used words. #[serde(skip)] pub staleness: Option>>, } impl Default for GenerationConfig { fn default() -> Self { Self { width: 15, height: 15, symmetry: SymmetryType::default(), black_cell_density: 0.22, min_word_length: 3, frequency_threshold: 0.0, obscurity_budget: 0.20, max_backtrack: 5000, max_restarts: 10, timeout_seconds: 90, seed: None, cryptic_grid: false, rand_top_base: 10, cancelled: None, staleness: None, } } } impl GenerationConfig { pub fn rand_top_for_size(size: usize) -> usize { match size { 0..=5 => 20, 6..=9 => 15, 10..=14 => 10, _ => 8, } } pub fn from_preset(preset: DifficultyPreset) -> Self { let size = preset.grid_size(); let (bt, restarts) = match preset { DifficultyPreset::Codex => (200000, 40), DifficultyPreset::Grimoire => (500000, 60), _ => (5000, 10), }; Self { width: size, height: size, black_cell_density: preset.black_cell_density(), frequency_threshold: preset.frequency_threshold(), obscurity_budget: preset.obscurity_budget(), max_backtrack: bt, max_restarts: restarts, rand_top_base: preset.rand_top_base(), ..Default::default() } } }