1use std::collections::{BTreeMap, BTreeSet};
20
21use crate::envelope::Campaign;
22use crate::ids::NodeId;
23
24use super::{Ground, PlacedBox, PlacedSeam};
25
26pub type Aabb = ([i64; 3], [i64; 3]);
28
29pub type Contest = (Vec<NodeId>, Vec<[i64; 3]>);
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
34pub enum Owner {
35 Place(NodeId),
37 Ground,
40 Nobody,
42 Contested(Vec<NodeId>),
45}
46
47impl Owner {
48 #[must_use]
50 pub fn describe(&self) -> String {
51 match self {
52 Owner::Place(n) => format!("`{n}`"),
53 Owner::Ground => "the whole's fixed ground".to_string(),
54 Owner::Nobody => "nobody (the site's fill)".to_string(),
55 Owner::Contested(ns) => format!(
56 "no one — contested by {}",
57 ns.iter()
58 .map(|n| format!("`{n}`"))
59 .collect::<Vec<_>>()
60 .join(" and ")
61 ),
62 }
63 }
64}
65
66fn inside(cell: [i64; 3], (lo, hi): Aabb) -> bool {
67 (0..3).all(|i| cell[i] >= lo[i] && cell[i] <= hi[i])
68}
69
70fn intersect(a: Aabb, b: Aabb) -> Option<Aabb> {
71 let lo = [a.0[0].max(b.0[0]), a.0[1].max(b.0[1]), a.0[2].max(b.0[2])];
72 let hi = [a.1[0].min(b.1[0]), a.1[1].min(b.1[1]), a.1[2].min(b.1[2])];
73 (0..3).all(|i| lo[i] <= hi[i]).then_some((lo, hi))
74}
75
76fn cells(a: Aabb) -> impl Iterator<Item = [i64; 3]> {
77 let (lo, hi) = a;
78 (lo[0]..=hi[0]).flat_map(move |x| {
79 (lo[1]..=hi[1]).flat_map(move |y| (lo[2]..=hi[2]).map(move |z| [x, y, z]))
80 })
81}
82
83impl PlacedBox {
84 #[must_use]
86 pub fn top(&self) -> i64 {
87 self.floor + i64::from(self.clearance) - 1
88 }
89
90 #[must_use]
93 pub fn shell_top(&self) -> i64 {
94 if self.open {
95 self.top()
96 } else {
97 self.top() + 1
98 }
99 }
100
101 #[must_use]
106 pub fn roof_zone(&self) -> Option<Aabb> {
107 let roof = self.roof?;
108 if self.open {
109 return None;
110 }
111 let e = i64::from(roof.eaves);
112 let lid = self.top() + 1;
113 Some((
114 [self.foot[0] - 1 - e, lid, self.foot[2] - 1 - e],
115 [
116 self.foot[1] + 1 + e,
117 lid + i64::from(roof.courses),
118 self.foot[3] + 1 + e,
119 ],
120 ))
121 }
122
123 #[must_use]
125 pub fn claim_top(&self) -> i64 {
126 self.roof_zone().map_or(self.shell_top(), |(_, hi)| hi[1])
127 }
128
129 #[must_use]
131 pub fn floor_course_y(&self) -> i64 {
132 self.floor - 1
133 }
134
135 #[must_use]
138 pub fn is_ring_column(&self, x: i64, z: i64) -> bool {
139 let [x0, x1, z0, z1] = self.foot;
140 let in_shell = x >= x0 - 1 && x <= x1 + 1 && z >= z0 - 1 && z <= z1 + 1;
141 let in_foot = x >= x0 && x <= x1 && z >= z0 && z <= z1;
142 in_shell && !in_foot
143 }
144}
145
146#[derive(Debug, Clone)]
149pub struct Site<'a> {
150 pub boxes: &'a [PlacedBox],
152 pub seams: &'a [PlacedSeam],
154 pub ground: &'a Ground,
156 bottoms: Vec<i64>,
158 columns: Vec<Vec<i64>>,
161}
162
163impl PlacedBox {
164 #[must_use]
166 pub fn aloft(&self) -> Option<u32> {
167 self.base.aloft()
168 }
169
170 #[must_use]
173 pub fn in_shell_footprint(&self, x: i64, z: i64) -> bool {
174 let [x0, x1, z0, z1] = self.foot;
175 x >= x0 - 1 && x <= x1 + 1 && z >= z0 - 1 && z <= z1 + 1
176 }
177}
178
179impl<'a> Site<'a> {
180 #[must_use]
194 pub fn new(boxes: &'a [PlacedBox], seams: &'a [PlacedSeam], ground: &'a Ground) -> Site<'a> {
195 let columns: Vec<Vec<i64>> = boxes
196 .iter()
197 .map(|b| {
198 let [x0, x1, z0, z1] = b.foot;
199 let course = b.floor_course_y();
200 let mut out = Vec::new();
201 for x in x0 - 1..=x1 + 1 {
202 for z in z0 - 1..=z1 + 1 {
203 if let Some(n) = b.aloft() {
204 out.push(course - i64::from(n));
205 continue;
206 }
207 let mut bottom = course;
208 if ground.is_open()
209 && let Some(t) = ground.top(x, z)
210 {
211 bottom = bottom.min(t);
212 }
213 for q in boxes {
214 if q.node != b.node
215 && q.in_shell_footprint(x, z)
216 && q.claim_top() < b.floor
217 {
218 bottom = bottom.max(q.claim_top() + 1);
219 }
220 }
221 out.push(bottom.min(course));
222 }
223 }
224 out
225 })
226 .collect();
227 let bottoms = columns
228 .iter()
229 .map(|c| c.iter().copied().min().unwrap_or(i64::MAX))
230 .collect();
231 Site {
232 boxes,
233 seams,
234 ground,
235 bottoms,
236 columns,
237 }
238 }
239
240 #[must_use]
242 pub fn index_of(&self, node: &NodeId) -> Option<usize> {
243 self.boxes.iter().position(|b| &b.node == node)
244 }
245
246 #[must_use]
248 pub fn bottom(&self, i: usize) -> i64 {
249 self.bottoms[i]
250 }
251
252 #[must_use]
255 pub fn column_bottom(&self, i: usize, x: i64, z: i64) -> Option<i64> {
256 let b = &self.boxes[i];
257 if !b.in_shell_footprint(x, z) {
258 return None;
259 }
260 let depth = b.foot[3] - b.foot[2] + 3;
261 let k = (x - (b.foot[0] - 1)) * depth + (z - (b.foot[2] - 1));
262 usize::try_from(k).ok().map(|k| self.columns[i][k])
263 }
264
265 #[must_use]
267 pub fn is_aloft(&self, i: usize) -> bool {
268 self.boxes[i].aloft().is_some()
269 }
270
271 #[must_use]
276 pub fn shell(&self, i: usize) -> Aabb {
277 let b = &self.boxes[i];
278 (
279 [b.foot[0] - 1, self.bottoms[i], b.foot[2] - 1],
280 [b.foot[1] + 1, b.shell_top(), b.foot[3] + 1],
281 )
282 }
283
284 fn in_shell(&self, i: usize, cell: [i64; 3]) -> bool {
287 inside(cell, self.shell(i))
288 && self
289 .column_bottom(i, cell[0], cell[2])
290 .is_some_and(|b| cell[1] >= b)
291 }
292
293 #[must_use]
295 pub fn claim_bounds(&self, i: usize) -> Aabb {
296 let (mut lo, mut hi) = self.shell(i);
297 if let Some((zlo, zhi)) = self.boxes[i].roof_zone() {
298 for a in 0..3 {
299 lo[a] = lo[a].min(zlo[a]);
300 hi[a] = hi[a].max(zhi[a]);
301 }
302 }
303 (lo, hi)
304 }
305
306 #[must_use]
311 pub fn aloft_in_earth(&self, i: usize) -> Vec<[i64; 3]> {
312 let b = &self.boxes[i];
313 if b.aloft().is_none() || !self.ground.is_open() {
314 return Vec::new();
315 }
316 let bottom = self.bottoms[i];
317 let [x0, x1, z0, z1] = b.foot;
318 let mut out = Vec::new();
319 for x in x0 - 1..=x1 + 1 {
320 for z in z0 - 1..=z1 + 1 {
321 if let Some(t) = self.ground.top(x, z)
322 && t >= bottom
323 {
324 out.push([x, z, t]);
325 }
326 }
327 }
328 out
329 }
330
331 #[must_use]
341 pub fn ground_height(&self, i: usize, x: i64, z: i64) -> i64 {
342 let b = &self.boxes[i];
343 let terrain = self.terrain_height(i, x, z);
344 for s in self.seams {
345 if s.normal_axis == 1 || (s.a != b.node && s.b != b.node) {
346 continue;
347 }
348 let along = if s.normal_axis == 0 { z } else { x };
349 let across = if s.normal_axis == 0 { x } else { z };
350 let other = if s.normal_axis == 0 { 2 } else { 0 };
351 if across == s.plane
352 && along >= s.opening.0[other]
353 && along <= s.opening.1[other]
354 && s.opening.0[1] - 1 <= terrain + 1
355 {
356 return s.opening.0[1] - 1;
357 }
358 }
359 terrain
360 }
361
362 #[must_use]
365 pub fn terrain_height(&self, i: usize, x: i64, z: i64) -> i64 {
366 if self.ground.is_open()
367 && let Some(t) = self.ground.top(x, z)
368 {
369 return t;
370 }
371 self.boxes[i].floor_course_y()
372 }
373
374 #[must_use]
377 pub fn is_fixed(&self, i: usize, cell: [i64; 3]) -> bool {
378 !self.is_aloft(i)
379 && self.in_shell(i, cell)
380 && self.boxes[i].is_ring_column(cell[0], cell[2])
381 && cell[1] <= self.ground_height(i, cell[0], cell[2])
382 }
383
384 #[must_use]
389 pub fn is_sky_ring(&self, i: usize, cell: [i64; 3]) -> bool {
390 let b = &self.boxes[i];
391 b.open
392 && !self.is_aloft(i)
393 && b.is_ring_column(cell[0], cell[2])
394 && cell[1] > b.floor_course_y()
395 && cell[1] <= b.top()
396 }
397
398 #[must_use]
402 pub fn claims(&self, i: usize, cell: [i64; 3]) -> bool {
403 if self.in_shell(i, cell) {
404 return true;
405 }
406 let b = &self.boxes[i];
407 let Some(zone) = b.roof_zone() else {
408 return false;
409 };
410 if !inside(cell, zone) {
411 return false;
412 }
413 let over_shell = cell[0] >= b.foot[0] - 1
414 && cell[0] <= b.foot[1] + 1
415 && cell[2] >= b.foot[2] - 1
416 && cell[2] <= b.foot[3] + 1;
417 over_shell || !(0..self.boxes.len()).any(|k| k != i && self.in_shell(k, cell))
420 }
421
422 #[must_use]
425 pub fn near(&self, bounds: Aabb) -> Vec<usize> {
426 (0..self.boxes.len())
427 .filter(|i| intersect(self.claim_bounds(*i), bounds).is_some())
428 .collect()
429 }
430
431 #[must_use]
434 pub fn owner(&self, cell: [i64; 3]) -> Owner {
435 let all: Vec<usize> = (0..self.boxes.len()).collect();
436 self.owner_among(cell, &all)
437 }
438
439 #[must_use]
458 pub fn owner_among(&self, cell: [i64; 3], cand: &[usize]) -> Owner {
459 if cand.iter().any(|&i| self.is_fixed(i, cell)) {
460 return Owner::Ground;
461 }
462 if let Some(&i) = cand.iter().find(|&&i| inside(cell, self.boxes[i].space())) {
463 return Owner::Place(self.boxes[i].node.clone());
464 }
465 let claimants: Vec<usize> = cand
466 .iter()
467 .copied()
468 .filter(|&i| self.claims(i, cell))
469 .collect();
470 match claimants.len() {
471 0 => return Owner::Nobody,
472 1 => return Owner::Place(self.boxes[claimants[0]].node.clone()),
473 _ => {}
474 }
475 let floors: Vec<usize> = claimants
477 .iter()
478 .copied()
479 .filter(|&i| cell[1] == self.boxes[i].floor_course_y())
480 .collect();
481 if floors.len() == 1 {
482 return Owner::Place(self.boxes[floors[0]].node.clone());
483 }
484 let roofed: Vec<usize> = claimants
486 .iter()
487 .copied()
488 .filter(|&i| !self.boxes[i].open)
489 .collect();
490 if roofed.len() == 1 {
491 return Owner::Place(self.boxes[roofed[0]].node.clone());
492 }
493 let names_of: Vec<&NodeId> = claimants.iter().map(|&i| &self.boxes[i].node).collect();
503 let across: Vec<&PlacedSeam> = self
504 .seams
505 .iter()
506 .filter(|s| {
507 names_of.contains(&&s.a)
508 && names_of.contains(&&s.b)
509 && cell[s.normal_axis] == s.plane
510 })
511 .collect();
512 let pair_disagrees = across.iter().any(|s| {
513 across.iter().any(|t| {
514 let same_pair = (s.a == t.a && s.b == t.b) || (s.a == t.b && s.b == t.a);
515 same_pair && s.a != t.a
516 })
517 });
518 if let Some(first) = across.first()
519 && !pair_disagrees
520 {
521 return Owner::Place(first.a.clone());
522 }
523 let aloft: Vec<usize> = claimants
530 .iter()
531 .copied()
532 .filter(|&i| self.is_aloft(i))
533 .collect();
534 if aloft.len() == 1
535 && claimants
536 .iter()
537 .all(|&i| i == aloft[0] || self.is_sky_ring(i, cell))
538 {
539 return Owner::Place(self.boxes[aloft[0]].node.clone());
540 }
541 let mut names: Vec<NodeId> = claimants
542 .iter()
543 .map(|&i| self.boxes[i].node.clone())
544 .collect();
545 names.sort();
546 Owner::Contested(names)
547 }
548
549 #[must_use]
551 pub fn claim_cells(&self, i: usize) -> Vec<[i64; 3]> {
552 cells(self.claim_bounds(i))
553 .filter(|c| self.claims(i, *c))
554 .collect()
555 }
556
557 #[must_use]
561 pub fn fixed_cells(&self, i: usize) -> Vec<([i64; 3], i64)> {
562 let b = &self.boxes[i];
563 let (lo, hi) = self.shell(i);
564 let mut out = Vec::new();
565 if self.is_aloft(i) {
566 return out;
567 }
568 for x in lo[0]..=hi[0] {
569 for z in lo[2]..=hi[2] {
570 if !b.is_ring_column(x, z) {
571 continue;
572 }
573 let g = self.ground_height(i, x, z);
574 let from = self.column_bottom(i, x, z).unwrap_or(lo[1]);
575 for y in from..=g.min(hi[1]) {
576 out.push(([x, y, z], g));
577 }
578 }
579 }
580 out
581 }
582
583 #[must_use]
587 pub fn contests(&self) -> (Vec<Contest>, usize) {
588 let mut by: BTreeMap<Vec<NodeId>, BTreeSet<[i64; 3]>> = BTreeMap::new();
589 let mut awarded: BTreeSet<[i64; 3]> = BTreeSet::new();
590 for i in 0..self.boxes.len() {
591 for j in i + 1..self.boxes.len() {
592 let Some(meet) = intersect(self.claim_bounds(i), self.claim_bounds(j)) else {
593 continue;
594 };
595 let cand = self.near(meet);
596 for c in cells(meet) {
597 if !(self.claims(i, c) && self.claims(j, c)) {
598 continue;
599 }
600 match self.owner_among(c, &cand) {
601 Owner::Contested(names) => {
602 by.entry(names).or_default().insert(c);
603 }
604 _ => {
605 awarded.insert(c);
606 }
607 }
608 }
609 }
610 }
611 (
612 by.into_iter()
613 .map(|(n, c)| (n, c.into_iter().collect()))
614 .collect(),
615 awarded.len(),
616 )
617 }
618}
619
620#[must_use]
625pub fn merge_cells(cells: &BTreeSet<[i64; 3]>) -> Vec<Aabb> {
626 let mut left = cells.clone();
627 let mut out = Vec::new();
628 while let Some(&lo) = left.iter().next() {
629 let mut hi = lo;
630 while left.contains(&[lo[0], lo[1], hi[2] + 1]) {
631 hi[2] += 1;
632 }
633 'y: loop {
634 let ny = hi[1] + 1;
635 for z in lo[2]..=hi[2] {
636 if !left.contains(&[lo[0], ny, z]) {
637 break 'y;
638 }
639 }
640 hi[1] = ny;
641 }
642 'x: loop {
643 let nx = hi[0] + 1;
644 for y in lo[1]..=hi[1] {
645 for z in lo[2]..=hi[2] {
646 if !left.contains(&[nx, y, z]) {
647 break 'x;
648 }
649 }
650 }
651 hi[0] = nx;
652 }
653 for x in lo[0]..=hi[0] {
654 for y in lo[1]..=hi[1] {
655 for z in lo[2]..=hi[2] {
656 left.remove(&[x, y, z]);
657 }
658 }
659 }
660 out.push((lo, hi));
661 }
662 out
663}
664
665#[derive(Debug, Clone, PartialEq, Eq)]
668pub struct Void {
669 pub lo: [i64; 3],
671 pub hi: [i64; 3],
673 pub owner: Owner,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq)]
680pub struct Ownership {
681 pub lo: [i64; 3],
683 pub hi: [i64; 3],
685 pub owned: Vec<Aabb>,
687 pub owned_cells: usize,
689 pub voids: Vec<Void>,
692 pub fixed: Vec<([i64; 3], String)>,
695 pub clipped: Vec<(Aabb, NodeId)>,
698}
699
700impl Site<'_> {
701 #[must_use]
703 pub fn ownership(&self, i: usize) -> Ownership {
704 let b = &self.boxes[i];
705 let me = Owner::Place(b.node.clone());
706 let bounds = self.claim_bounds(i);
707 let cand = self.near(bounds);
708 let owned: BTreeSet<[i64; 3]> = self
709 .claim_cells(i)
710 .into_iter()
711 .filter(|c| self.owner_among(*c, &cand) == me)
712 .collect();
713 let (lo, hi) = owned
714 .iter()
715 .fold(([i64::MAX; 3], [i64::MIN; 3]), |(mut lo, mut hi), c| {
716 for a in 0..3 {
717 lo[a] = lo[a].min(c[a]);
718 hi[a] = hi[a].max(c[a]);
719 }
720 (lo, hi)
721 });
722 let mut by_owner: BTreeMap<Owner, BTreeSet<[i64; 3]>> = BTreeMap::new();
723 let mut fixed: Vec<([i64; 3], String)> = Vec::new();
724 if !owned.is_empty() {
725 let fcand = self.near((lo, hi));
726 for c in cells((lo, hi)) {
727 if owned.contains(&c) {
728 continue;
729 }
730 let o = self.owner_among(c, &fcand);
731 if o == Owner::Ground {
732 let g = fcand
733 .iter()
734 .find(|&&k| self.is_fixed(k, c))
735 .map_or(c[1], |&k| self.ground_height(k, c[0], c[2]));
736 fixed.push((c, self.ground.ground_block(c, g).to_string()));
737 }
738 by_owner.entry(o).or_default().insert(c);
739 }
740 }
741 let voids: Vec<Void> = by_owner
742 .iter()
743 .flat_map(|(o, cells)| {
744 merge_cells(cells).into_iter().map(move |(lo, hi)| Void {
745 lo,
746 hi,
747 owner: o.clone(),
748 })
749 })
750 .collect();
751 let mut clipped_by: BTreeMap<NodeId, BTreeSet<[i64; 3]>> = BTreeMap::new();
754 if let Some(zone) = b.roof_zone() {
755 for c in cells(zone) {
756 let over_shell = c[0] >= b.foot[0] - 1
757 && c[0] <= b.foot[1] + 1
758 && c[2] >= b.foot[2] - 1
759 && c[2] <= b.foot[3] + 1;
760 if over_shell {
761 continue;
762 }
763 if let Some(k) = (0..self.boxes.len()).find(|&k| k != i && self.in_shell(k, c)) {
764 clipped_by
765 .entry(self.boxes[k].node.clone())
766 .or_default()
767 .insert(c);
768 }
769 }
770 }
771 let clipped: Vec<(Aabb, NodeId)> = clipped_by
772 .iter()
773 .flat_map(|(n, cells)| merge_cells(cells).into_iter().map(move |r| (r, n.clone())))
774 .collect();
775 Ownership {
776 lo,
777 hi,
778 owned_cells: owned.len(),
779 owned: merge_cells(&owned),
780 voids,
781 fixed,
782 clipped,
783 }
784 }
785}
786
787#[derive(Debug, Clone)]
789pub struct SitePlan {
790 pub boxes: Vec<PlacedBox>,
792 pub seams: Vec<PlacedSeam>,
794 pub ground: Ground,
796}
797
798impl SitePlan {
799 #[must_use]
801 pub fn of(c: &Campaign) -> SitePlan {
802 let mut reads = crate::metrics::Reads::new();
803 let boxes = super::placed_boxes(c, &mut reads);
804 let seams = super::placed_seams(c, &boxes, &mut reads);
805 SitePlan {
806 boxes,
807 seams,
808 ground: Ground::of(c),
809 }
810 }
811
812 #[must_use]
814 pub fn site(&self) -> Site<'_> {
815 Site::new(&self.boxes, &self.seams, &self.ground)
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use crate::ids::EdgeId;
823 use crate::siteplan::{Crossing, Face, Roof};
824
825 fn a_box(node: &str, foot: [i64; 4], floor: i64, clearance: u32, open: bool) -> PlacedBox {
826 PlacedBox {
827 node: NodeId(node.into()),
828 foot,
829 floor,
830 clearance,
831 open,
832 base: crate::siteplan::Base::Ground,
833 roof: None,
834 }
835 }
836
837 fn seam(a: &str, b: &str, axis: usize, plane: i64) -> PlacedSeam {
838 PlacedSeam {
839 edge: EdgeId(format!("edge/{}-{}", &a[5..], &b[5..])),
840 class: "walk",
841 a: NodeId(a.into()),
842 b: NodeId(b.into()),
843 face: Face::East,
844 normal_axis: axis,
845 plane,
846 opening: ([plane, 64, 2], [plane, 66, 3]),
847 shared: ([plane, 64, 0], [plane, 67, 7]),
848 crossing: Crossing::Portal,
849 rise: 0,
850 stair_in: None,
851 form: "a doorway".into(),
852 }
853 }
854
855 fn solid() -> Ground {
856 Ground::solid("minecraft:stone")
857 }
858
859 fn house_and_street() -> Vec<PlacedBox> {
861 vec![
862 a_box("node/house", [0, 7, 0, 7], 64, 4, false),
863 a_box("node/street", [9, 16, 0, 7], 64, 6, true),
864 ]
865 }
866
867 #[test]
876 fn an_aloft_seam_fixes_no_earth_under_it_and_a_grade_seam_levels() {
877 let boxes = vec![
878 a_box("node/east-house", [0, 7, 0, 7], 64, 4, false),
879 a_box("node/west-house", [9, 16, 0, 7], 64, 4, false),
880 ];
881 let seams = vec![seam("node/east-house", "node/west-house", 0, 8)];
882 let low = Ground::open_flat(47, "minecraft:grass_block", "minecraft:dirt");
883 let site = Site::new(&boxes, &seams, &low);
884 assert_eq!(site.ground_height(0, 8, 2), 47, "the terrain, not the deck");
885 assert!(
886 !site.is_fixed(0, [8, 55, 2]),
887 "no earth column under the bridge"
888 );
889 assert_eq!(
890 site.owner([8, 55, 2]),
891 Owner::Place(NodeId("node/east-house".into())),
892 "the column between terrain and deck is the seam's a"
893 );
894 assert!(
895 site.is_fixed(0, [8, 47, 2]),
896 "the terrain itself is still fixed"
897 );
898 let grade = Ground::open_flat(62, "minecraft:grass_block", "minecraft:dirt");
899 let site = Site::new(&boxes, &seams, &grade);
900 assert_eq!(
901 site.ground_height(0, 8, 2),
902 63,
903 "one step up: levelled to the sill"
904 );
905 assert!(site.is_fixed(0, [8, 63, 2]));
906 }
907
908 #[test]
909 fn the_party_plane_between_a_house_and_a_street_is_the_facade() {
910 let boxes = house_and_street();
911 let g = solid();
912 let site = Site::new(&boxes, &[], &g);
913 assert_eq!(
914 site.owner([8, 65, 3]),
915 Owner::Place(NodeId("node/house".into()))
916 );
917 assert_eq!(
918 site.owner([8, 63, 3]),
919 Owner::Ground,
920 "the ring's ground is fixed"
921 );
922 assert_eq!(
923 site.owner([8, 69, 3]),
924 Owner::Place(NodeId("node/street".into()))
925 );
926 assert_eq!(
927 site.owner([17, 65, 3]),
928 Owner::Place(NodeId("node/street".into()))
929 );
930 assert_eq!(site.owner([30, 65, 3]), Owner::Nobody);
931 }
932
933 #[test]
934 fn two_yards_meet_on_the_side_their_connection_names_first() {
935 let boxes = vec![
936 a_box("node/yard-a", [0, 7, 0, 7], 64, 3, true),
937 a_box("node/yard-b", [9, 16, 0, 7], 64, 3, true),
938 ];
939 let g = solid();
940 let site = Site::new(&boxes, &[], &g);
941 assert!(
942 matches!(site.owner([8, 65, 1]), Owner::Contested(_)),
943 "nothing connects them"
944 );
945 let s = [seam("node/yard-b", "node/yard-a", 0, 8)];
946 let site = Site::new(&boxes, &s, &g);
947 assert_eq!(
948 site.owner([8, 65, 1]),
949 Owner::Place(NodeId("node/yard-b".into()))
950 );
951 let disagree = [
952 seam("node/yard-b", "node/yard-a", 0, 8),
953 seam("node/yard-a", "node/yard-b", 0, 8),
954 ];
955 let site = Site::new(&boxes, &disagree, &g);
956 assert!(matches!(site.owner([8, 65, 1]), Owner::Contested(_)));
957 }
958
959 #[test]
960 fn a_stacked_plane_is_the_upper_floor() {
961 let lower = a_box("node/crypt", [0, 7, 0, 7], 60, 3, false); let upper = a_box("node/chapel", [0, 7, 0, 7], 64, 6, false); let boxes = vec![lower, upper];
964 let g = solid();
965 let site = Site::new(&boxes, &[], &g);
966 assert_eq!(
967 site.owner([3, 63, 3]),
968 Owner::Place(NodeId("node/chapel".into()))
969 );
970 }
971
972 #[test]
973 fn a_roof_zone_is_claimed_and_its_eaves_stop_at_a_neighbour() {
974 let mut boxes = house_and_street();
975 boxes[0].roof = Some(Roof {
976 courses: 4,
977 eaves: 1,
978 });
979 let g = solid();
980 let site = Site::new(&boxes, &[], &g);
981 assert_eq!(boxes[0].roof_zone(), Some(([-2, 68, -2], [9, 72, 9])));
982 assert!(!site.claims(0, [9, 68, 3]));
983 assert_eq!(
984 site.owner([9, 68, 3]),
985 Owner::Place(NodeId("node/street".into()))
986 );
987 assert!(site.claims(0, [9, 71, 3]));
988 assert_eq!(
989 site.owner([9, 71, 3]),
990 Owner::Place(NodeId("node/house".into()))
991 );
992 let o = site.ownership(0);
993 assert_eq!(o.lo, [-2, 63, -2]);
994 assert_eq!(o.hi, [9, 72, 9]);
995 assert!(o.clipped.iter().any(|(_, n)| n.0 == "node/street"));
996 let owned: BTreeSet<[i64; 3]> = o.owned.iter().flat_map(|a| cells(*a)).collect();
997 assert_eq!(owned.len(), o.owned_cells, "the merge is exact");
998 for v in &o.voids {
999 assert!(!owned.contains(&v.lo));
1000 }
1001 }
1002
1003 #[test]
1010 fn two_roofed_places_one_apart_are_contested_and_each_remedy_parts_them() {
1011 let g = solid();
1012 let a = a_box("node/west-room", [0, 7, 0, 7], 64, 4, false);
1013 let b = a_box("node/east-room", [9, 16, 0, 7], 64, 4, false);
1014 let boxes = vec![a.clone(), b.clone()];
1015 let site = Site::new(&boxes, &[], &g);
1016 let (contests, _) = site.contests();
1017 assert_eq!(contests.len(), 1, "one pair contests");
1018 assert!(!contests[0].1.is_empty(), "and the claims really intersect");
1019 assert!(
1020 contests[0].1.iter().all(|c| c[0] == 8),
1021 "in the shared ring column"
1022 );
1023
1024 let s = [seam("node/west-room", "node/east-room", 0, 8)];
1025 let site = Site::new(&boxes, &s, &g);
1026 assert!(
1027 site.contests().0.is_empty(),
1028 "a connection awards the plane"
1029 );
1030 assert_eq!(
1031 site.owner([8, 65, 5]),
1032 Owner::Place(NodeId("node/west-room".into()))
1033 );
1034
1035 let apart = vec![
1036 a.clone(),
1037 a_box("node/east-room", [10, 17, 0, 7], 64, 4, false),
1038 ];
1039 let site = Site::new(&apart, &[], &g);
1040 assert!(site.contests().0.is_empty(), "two apart, nothing is shared");
1041 assert_eq!(
1042 site.owner([9, 65, 5]),
1043 Owner::Place(NodeId("node/east-room".into()))
1044 );
1045
1046 let one = vec![a_box("node/one-room", [0, 16, 0, 7], 64, 4, false)];
1047 assert!(Site::new(&one, &[], &g).contests().0.is_empty());
1048 }
1049
1050 fn hung(mut b: PlacedBox, n: u32) -> PlacedBox {
1051 b.base = crate::siteplan::Base::Aloft(n);
1052 b
1053 }
1054
1055 fn grass(top: i64) -> Ground {
1056 Ground::open_flat(top, "minecraft:grass_block", "minecraft:dirt")
1057 }
1058
1059 #[test]
1067 fn a_hall_over_a_cellar_keeps_its_ground_beside_the_cellar() {
1068 let boxes = vec![
1069 a_box("node/hall", [0, 15, 0, 15], 64, 8, false),
1070 a_box("node/cellar", [2, 9, 2, 9], 58, 3, false),
1071 ];
1072 let g = grass(59);
1073 let site = Site::new(&boxes, &[], &g);
1074 assert_eq!(site.column_bottom(0, 5, 5), Some(62), "over the cellar");
1075 assert_eq!(site.column_bottom(0, 12, 12), Some(59), "beside it");
1076 assert_eq!(site.bottom(0), 59, "the shell reaches the lowest column");
1077 assert!(
1078 !site.fixed_cells(0).is_empty(),
1079 "the hall's ring keeps its fixed ground"
1080 );
1081 assert!(site.is_fixed(0, [-1, 59, 5]));
1082 assert_eq!(
1083 site.owner([12, 60, 12]),
1084 Owner::Place(NodeId("node/hall".into())),
1085 "the earth beside the cellar is the hall's"
1086 );
1087 assert_eq!(
1088 site.owner([5, 61, 5]),
1089 Owner::Place(NodeId("node/cellar".into())),
1090 "the cellar's lid is the cellar's"
1091 );
1092 assert!(site.contests().0.is_empty());
1093 }
1094
1095 #[test]
1101 fn an_aloft_place_owns_no_ground_and_the_space_under_it_is_the_commons() {
1102 let boxes = vec![hung(a_box("node/bridge", [0, 7, 0, 3], 70, 3, true), 1)];
1103 let g = grass(59);
1104 let site = Site::new(&boxes, &[], &g);
1105 assert_eq!(site.bottom(0), 68);
1106 assert_eq!(site.column_bottom(0, -1, -1), Some(68));
1107 assert!(site.fixed_cells(0).is_empty(), "no fixed ring");
1108 assert!(!site.is_fixed(0, [-1, 59, 0]));
1109 assert_eq!(site.owner([3, 60, 1]), Owner::Nobody, "under the deck");
1110 assert_eq!(site.owner([3, 59, 1]), Owner::Nobody, "the terrain");
1111 assert_eq!(
1112 site.owner([3, 68, 1]),
1113 Owner::Place(NodeId("node/bridge".into())),
1114 "the underside course"
1115 );
1116 assert!(site.aloft_in_earth(0).is_empty());
1117 let high = grass(68);
1118 let site = Site::new(&boxes, &[], &high);
1119 assert_eq!(
1120 site.aloft_in_earth(0).len(),
1121 10 * 6,
1122 "every footprint and ring column"
1123 );
1124 let solid = solid();
1125 assert!(
1126 Site::new(&boxes, &[], &solid).aloft_in_earth(0).is_empty(),
1127 "a solid site's rock is carved by every claim"
1128 );
1129 }
1130
1131 #[test]
1139 fn a_gantry_over_a_yard_meets_it_at_the_yards_headroom() {
1140 let yard = a_box("node/yard", [0, 7, 0, 7], 64, 4, true);
1141 let gantry = a_box("node/gantry", [2, 5, -6, 13], 69, 3, true);
1142 let g = grass(63);
1143 let boxes = vec![yard.clone(), hung(gantry.clone(), 0)];
1144 let site = Site::new(&boxes, &[], &g);
1145 assert!(site.contests().0.is_empty(), "the cuboids touch");
1146 assert_eq!(
1147 site.owner([3, 67, 3]),
1148 Owner::Place(NodeId("node/yard".into()))
1149 );
1150 assert_eq!(
1151 site.owner([3, 68, 3]),
1152 Owner::Place(NodeId("node/gantry".into()))
1153 );
1154 let boxes = vec![yard, hung(gantry, 1)];
1155 let site = Site::new(&boxes, &[], &g);
1156 assert!(
1157 site.contests().0.is_empty(),
1158 "the hung place takes the sky ring"
1159 );
1160 assert_eq!(
1161 site.owner([3, 67, -1]),
1162 Owner::Place(NodeId("node/gantry".into())),
1163 "the yard's ring air under the gantry"
1164 );
1165 assert_eq!(
1166 site.owner([3, 67, 3]),
1167 Owner::Place(NodeId("node/yard".into())),
1168 "the yard's own sky"
1169 );
1170 }
1171
1172 #[test]
1184 fn a_place_hung_in_a_glades_sky_owns_what_it_hangs_in() {
1185 let g = grass(71);
1186 let glade = a_box("node/glade", [0, 21, 0, 21], 72, 13, true);
1187 let house = hung(a_box("node/house", [-2, 21, -2, 21], 86, 8, true), 0);
1188 let bridge = hung(a_box("node/bridge", [23, 44, 6, 8], 80, 9, true), 1);
1189 let boxes = vec![glade, house, bridge];
1190 let seams = vec![seam("node/house", "node/bridge", 0, 22)];
1191 let site = Site::new(&boxes, &seams, &g);
1192 let (contests, _) = site.contests();
1193 assert!(contests.is_empty(), "{contests:?}");
1194 for y in [78, 80, 81, 82, 83, 84] {
1195 for z in 5..=9 {
1196 assert!(site.is_sky_ring(0, [22, y, z]));
1197 assert_eq!(
1198 site.owner([22, y, z]),
1199 Owner::Place(NodeId("node/bridge".into())),
1200 "[22, {y}, {z}]"
1201 );
1202 }
1203 }
1204 assert_eq!(
1205 site.owner([22, 72, 7]),
1206 Owner::Place(NodeId("node/glade".into())),
1207 "the glade's ring under the bridge is still its own"
1208 );
1209 assert!(
1210 !site.is_sky_ring(0, [22, 71, 7]),
1211 "the floor course is not sky"
1212 );
1213 assert!(
1214 !site.is_sky_ring(0, [10, 78, 10]),
1215 "the play space is not ring"
1216 );
1217 assert!(
1218 !site.is_sky_ring(2, [22, 82, 7]),
1219 "a hung place's ring is not sky"
1220 );
1221 }
1222
1223 #[test]
1224 fn merge_is_exact_and_deterministic() {
1225 let cells: BTreeSet<[i64; 3]> = [[0, 0, 0], [0, 0, 1], [1, 0, 0], [1, 0, 1], [5, 5, 5]]
1226 .into_iter()
1227 .collect();
1228 let m = merge_cells(&cells);
1229 assert_eq!(m, vec![([0, 0, 0], [1, 0, 1]), ([5, 5, 5], [5, 5, 5])]);
1230 assert_eq!(merge_cells(&cells), m);
1231 }
1232}