Skip to main content

terrain_forge/
constraints.rs

1//! Constraint validation utilities and helpers.
2
3use crate::{pipeline, semantic};
4use crate::{Grid, Tile};
5use std::collections::HashMap;
6use std::collections::VecDeque;
7
8pub fn validate_connectivity(grid: &Grid<Tile>) -> f32 {
9    let (w, h) = (grid.width(), grid.height());
10    let mut visited = vec![false; w * h];
11    let mut regions = Vec::new();
12
13    for y in 0..h {
14        for x in 0..w {
15            let idx = y * w + x;
16            if grid[(x, y)].is_floor() && !visited[idx] {
17                let size = flood_fill(grid, &mut visited, x, y, w, h);
18                regions.push(size);
19            }
20        }
21    }
22
23    if regions.is_empty() {
24        return 0.0;
25    }
26
27    let largest = *regions.iter().max().unwrap_or(&0);
28    let total: usize = regions.iter().sum();
29
30    largest as f32 / total as f32
31}
32
33fn flood_fill(
34    grid: &Grid<Tile>,
35    visited: &mut [bool],
36    sx: usize,
37    sy: usize,
38    w: usize,
39    h: usize,
40) -> usize {
41    let mut queue = VecDeque::new();
42    queue.push_back((sx, sy));
43    let mut count = 0;
44
45    while let Some((x, y)) = queue.pop_front() {
46        let idx = y * w + x;
47        if visited[idx] || !grid[(x, y)].is_floor() {
48            continue;
49        }
50        visited[idx] = true;
51        count += 1;
52
53        if x > 0 {
54            queue.push_back((x - 1, y));
55        }
56        if x + 1 < w {
57            queue.push_back((x + 1, y));
58        }
59        if y > 0 {
60            queue.push_back((x, y - 1));
61        }
62        if y + 1 < h {
63            queue.push_back((x, y + 1));
64        }
65    }
66    count
67}
68
69pub fn validate_density(grid: &Grid<Tile>, min: f64, max: f64) -> bool {
70    let total = grid.width() * grid.height();
71    let floors = grid.count(|t| t.is_floor());
72    let density = floors as f64 / total as f64;
73    density >= min && density <= max
74}
75
76pub fn validate_border(grid: &Grid<Tile>) -> bool {
77    let (w, h) = (grid.width(), grid.height());
78    for x in 0..w {
79        if grid[(x, 0)].is_floor() || grid[(x, h - 1)].is_floor() {
80            return false;
81        }
82    }
83    for y in 0..h {
84        if grid[(0, y)].is_floor() || grid[(w - 1, y)].is_floor() {
85            return false;
86        }
87    }
88    true
89}
90
91/// Unified constraint kinds for evaluation
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum ConstraintKind {
94    Grid,
95    Semantic,
96    Pipeline,
97    Placement,
98    Custom,
99}
100
101/// Input context for constraint evaluation
102#[derive(Debug)]
103pub struct ConstraintContext<'a> {
104    pub grid: &'a Grid<Tile>,
105    pub semantic: Option<&'a semantic::SemanticLayers>,
106    pub pipeline: Option<&'a pipeline::PipelineContext>,
107    pub meta: Option<&'a HashMap<String, String>>,
108}
109
110impl<'a> ConstraintContext<'a> {
111    pub fn new(grid: &'a Grid<Tile>) -> Self {
112        Self {
113            grid,
114            semantic: None,
115            pipeline: None,
116            meta: None,
117        }
118    }
119}
120
121/// Result for a single constraint
122#[derive(Debug, Clone)]
123pub struct ConstraintResult {
124    pub passed: bool,
125    pub score: f32,
126    pub details: HashMap<String, String>,
127}
128
129impl ConstraintResult {
130    pub fn pass() -> Self {
131        Self {
132            passed: true,
133            score: 1.0,
134            details: HashMap::new(),
135        }
136    }
137
138    pub fn fail() -> Self {
139        Self {
140            passed: false,
141            score: 0.0,
142            details: HashMap::new(),
143        }
144    }
145
146    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
147        self.details.insert(key.into(), value.into());
148        self
149    }
150}
151
152/// Unified constraint trait
153pub trait Constraint: Send + Sync {
154    fn id(&self) -> &'static str;
155    fn kind(&self) -> ConstraintKind;
156    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult;
157}
158
159/// Evaluation record
160#[derive(Debug, Clone)]
161pub struct ConstraintEvaluation {
162    pub id: String,
163    pub kind: ConstraintKind,
164    pub result: ConstraintResult,
165}
166
167/// Report from evaluating a set of constraints
168#[derive(Debug, Clone)]
169pub struct ConstraintReport {
170    pub passed: bool,
171    pub results: Vec<ConstraintEvaluation>,
172}
173
174/// Collection of constraints
175#[derive(Default)]
176pub struct ConstraintSet {
177    constraints: Vec<Box<dyn Constraint>>,
178}
179
180impl ConstraintSet {
181    pub fn new() -> Self {
182        Self {
183            constraints: Vec::new(),
184        }
185    }
186
187    pub fn push<C: Constraint + 'static>(&mut self, constraint: C) {
188        self.constraints.push(Box::new(constraint));
189    }
190
191    pub fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintReport {
192        let mut results = Vec::new();
193        let mut passed = true;
194
195        for constraint in &self.constraints {
196            let result = constraint.evaluate(ctx);
197            if !result.passed {
198                passed = false;
199            }
200            results.push(ConstraintEvaluation {
201                id: constraint.id().to_string(),
202                kind: constraint.kind(),
203                result,
204            });
205        }
206
207        ConstraintReport { passed, results }
208    }
209}
210
211/// Adapter for SemanticRequirements
212pub struct SemanticRequirementsConstraint {
213    pub requirements: semantic::SemanticRequirements,
214}
215
216impl SemanticRequirementsConstraint {
217    pub fn new(requirements: semantic::SemanticRequirements) -> Self {
218        Self { requirements }
219    }
220}
221
222impl Constraint for SemanticRequirementsConstraint {
223    fn id(&self) -> &'static str {
224        "semantic_requirements"
225    }
226
227    fn kind(&self) -> ConstraintKind {
228        ConstraintKind::Semantic
229    }
230
231    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult {
232        match ctx.semantic {
233            Some(semantic) => {
234                if self.requirements.validate(semantic) {
235                    ConstraintResult::pass()
236                } else {
237                    ConstraintResult::fail()
238                }
239            }
240            None => ConstraintResult::fail().with_detail("semantic", "missing"),
241        }
242    }
243}
244
245/// Adapter for connectivity validation
246pub struct ConnectivityConstraint {
247    pub min_ratio: f32,
248}
249
250impl ConnectivityConstraint {
251    pub fn new(min_ratio: f32) -> Self {
252        Self { min_ratio }
253    }
254}
255
256impl Constraint for ConnectivityConstraint {
257    fn id(&self) -> &'static str {
258        "grid_connectivity"
259    }
260
261    fn kind(&self) -> ConstraintKind {
262        ConstraintKind::Grid
263    }
264
265    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult {
266        let ratio = validate_connectivity(ctx.grid);
267        let passed = ratio >= self.min_ratio;
268        let score = if self.min_ratio <= 0.0 {
269            1.0
270        } else {
271            (ratio / self.min_ratio).min(1.0)
272        };
273        ConstraintResult {
274            passed,
275            score,
276            details: HashMap::from([
277                ("ratio".to_string(), format!("{:.4}", ratio)),
278                ("min".to_string(), format!("{:.4}", self.min_ratio)),
279            ]),
280        }
281    }
282}
283
284/// Adapter for density validation
285pub struct DensityConstraint {
286    pub min: f64,
287    pub max: f64,
288}
289
290impl DensityConstraint {
291    pub fn new(min: f64, max: f64) -> Self {
292        Self { min, max }
293    }
294}
295
296impl Constraint for DensityConstraint {
297    fn id(&self) -> &'static str {
298        "grid_density"
299    }
300
301    fn kind(&self) -> ConstraintKind {
302        ConstraintKind::Grid
303    }
304
305    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult {
306        let total = ctx.grid.width() * ctx.grid.height();
307        let floors = ctx.grid.count(|t| t.is_floor());
308        let density = floors as f64 / total as f64;
309        let passed = validate_density(ctx.grid, self.min, self.max);
310        let score = if density < self.min {
311            (density / self.min).min(1.0) as f32
312        } else if density > self.max {
313            (self.max / density).min(1.0) as f32
314        } else {
315            1.0
316        };
317        ConstraintResult {
318            passed,
319            score,
320            details: HashMap::from([
321                ("density".to_string(), format!("{:.4}", density)),
322                ("min".to_string(), format!("{:.4}", self.min)),
323                ("max".to_string(), format!("{:.4}", self.max)),
324            ]),
325        }
326    }
327}
328
329/// Adapter for border validation
330pub struct BorderConstraint;
331
332impl Constraint for BorderConstraint {
333    fn id(&self) -> &'static str {
334        "grid_border"
335    }
336
337    fn kind(&self) -> ConstraintKind {
338        ConstraintKind::Grid
339    }
340
341    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult {
342        if validate_border(ctx.grid) {
343            ConstraintResult::pass()
344        } else {
345            ConstraintResult::fail()
346        }
347    }
348}
349
350/// Adapter for pipeline conditions
351pub struct PipelineConditionConstraint {
352    pub condition: pipeline::PipelineCondition,
353}
354
355impl PipelineConditionConstraint {
356    pub fn new(condition: pipeline::PipelineCondition) -> Self {
357        Self { condition }
358    }
359}
360
361impl Constraint for PipelineConditionConstraint {
362    fn id(&self) -> &'static str {
363        "pipeline_condition"
364    }
365
366    fn kind(&self) -> ConstraintKind {
367        ConstraintKind::Pipeline
368    }
369
370    fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult {
371        match ctx.pipeline {
372            Some(pipeline_ctx) => {
373                if self.condition.evaluate(ctx.grid, pipeline_ctx) {
374                    ConstraintResult::pass()
375                } else {
376                    ConstraintResult::fail()
377                }
378            }
379            None => ConstraintResult::fail().with_detail("pipeline", "missing"),
380        }
381    }
382}