diff --git a/cruciverb-core/src/config.rs b/cruciverb-core/src/config.rs new file mode 100644 index 0000000..4672d07 --- /dev/null +++ b/cruciverb-core/src/config.rs @@ -0,0 +1,169 @@ +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() + } + } +}