1use crate::{pipeline, semantic};
4use crate::{Grid, Tile};
5use std::collections::HashMap;
6
7#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum ConstraintKind {
48 Grid,
50 Semantic,
52 Pipeline,
54 Placement,
56 Custom,
58}
59
60#[derive(Debug)]
62pub struct ConstraintContext<'a> {
63 pub grid: &'a Grid<Tile>,
65 pub semantic: Option<&'a semantic::SemanticLayers>,
67 pub pipeline: Option<&'a pipeline::PipelineContext>,
69 pub meta: Option<&'a HashMap<String, String>>,
71}
72
73impl<'a> ConstraintContext<'a> {
74 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#[derive(Debug, Clone)]
87pub struct ConstraintResult {
88 pub passed: bool,
90 pub score: f32,
92 pub details: HashMap<String, String>,
94}
95
96impl ConstraintResult {
97 pub fn pass() -> Self {
99 Self {
100 passed: true,
101 score: 1.0,
102 details: HashMap::new(),
103 }
104 }
105
106 pub fn fail() -> Self {
108 Self {
109 passed: false,
110 score: 0.0,
111 details: HashMap::new(),
112 }
113 }
114
115 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
122pub trait Constraint: Send + Sync {
124 fn id(&self) -> &'static str;
126 fn kind(&self) -> ConstraintKind;
128 fn evaluate(&self, ctx: &ConstraintContext) -> ConstraintResult;
130}
131
132#[derive(Debug, Clone)]
134pub struct ConstraintEvaluation {
135 pub id: String,
137 pub kind: ConstraintKind,
139 pub result: ConstraintResult,
141}
142
143#[derive(Debug, Clone)]
145pub struct ConstraintReport {
146 pub passed: bool,
148 pub results: Vec<ConstraintEvaluation>,
150}
151
152#[derive(Default)]
154pub struct ConstraintSet {
155 constraints: Vec<Box<dyn Constraint>>,
156}
157
158impl ConstraintSet {
159 pub fn new() -> Self {
161 Self {
162 constraints: Vec::new(),
163 }
164 }
165
166 pub fn push<C: Constraint + 'static>(&mut self, constraint: C) {
168 self.constraints.push(Box::new(constraint));
169 }
170
171 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
192pub struct SemanticRequirementsConstraint {
194 pub requirements: semantic::SemanticRequirements,
196}
197
198impl SemanticRequirementsConstraint {
199 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
228pub struct ConnectivityConstraint {
230 pub min_ratio: f32,
232}
233
234impl ConnectivityConstraint {
235 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
269pub struct DensityConstraint {
271 pub min: f64,
273 pub max: f64,
275}
276
277impl DensityConstraint {
278 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
317pub 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
338pub struct PipelineConditionConstraint {
341 pub condition: pipeline::PipelineCondition,
343}
344
345impl PipelineConditionConstraint {
346 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}