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