1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum ConstraintKind {
94 Grid,
95 Semantic,
96 Pipeline,
97 Placement,
98 Custom,
99}
100
101#[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#[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
152pub 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#[derive(Debug, Clone)]
161pub struct ConstraintEvaluation {
162 pub id: String,
163 pub kind: ConstraintKind,
164 pub result: ConstraintResult,
165}
166
167#[derive(Debug, Clone)]
169pub struct ConstraintReport {
170 pub passed: bool,
171 pub results: Vec<ConstraintEvaluation>,
172}
173
174#[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
211pub 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
245pub 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
284pub 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
329pub 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
350pub 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}