use crate::design::{OccupancyIndex, P3};
use crate::routing::engine::transport::{self, BlockView, Mechanism, Placement};
use pnr_core::astar::{route, RouteRequest};
use pnr_core::congestion::{route_all, CongestionOpts, NetReq};
use pnr_core::fabric::{Budget, Candidate, Fabric, RouteCtx, State};
use pnr_core::grid::{Aabb, Pos};
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap};
pub const MIN_LEG: u8 = 3;
#[derive(Copy, Clone, Debug)]
pub struct Effort {
pub turn_cost: u32,
pub margin: i32,
pub max_iter: usize,
}
pub const LADDER: [Effort; 2] = [
Effort {
turn_cost: 12,
margin: 24,
max_iter: 80_000,
},
Effort {
turn_cost: 4,
margin: 96,
max_iter: 400_000,
},
];
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Heading {
Start,
PlusX,
MinusX,
PlusZ,
MinusZ,
}
impl Heading {
fn delta(self) -> (i32, i32) {
match self {
Heading::Start => (0, 0),
Heading::PlusX => (1, 0),
Heading::MinusX => (-1, 0),
Heading::PlusZ => (0, 1),
Heading::MinusZ => (0, -1),
}
}
fn opposite(self) -> Heading {
match self {
Heading::Start => Heading::Start,
Heading::PlusX => Heading::MinusX,
Heading::MinusX => Heading::PlusX,
Heading::PlusZ => Heading::MinusZ,
Heading::MinusZ => Heading::PlusZ,
}
}
const ALL: [Heading; 4] = [
Heading::PlusX,
Heading::MinusX,
Heading::PlusZ,
Heading::MinusZ,
];
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Leg {
heading: Heading,
run: u8,
}
#[derive(Clone, Debug)]
pub struct NetForm {
y0: i32,
width: u8,
exempt: BTreeSet<(i32, i32)>,
bound: Aabb,
}
impl NetForm {
fn y_span(&self) -> (i32, i32) {
(self.y0 - 1, self.y0 + 2 * (self.width as i32 - 1))
}
}
pub struct BusFabric<'a> {
occ: &'a OccupancyIndex,
nets: Vec<NetForm>,
turn_cost: u32,
memo: RefCell<HashMap<(usize, i32, i32), bool>>,
hug_memo: RefCell<HashMap<(usize, i32, i32), bool>>,
reserved: Vec<BTreeSet<(i32, i32)>>,
hug_cost: u32,
}
const HUG_COST: u32 = 4;
fn hug_cost() -> u32 {
std::env::var("NUCLEATION_HUG_COST")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(HUG_COST)
}
const ESCAPE_COST: u32 = 200;
impl<'a> BusFabric<'a> {
fn column_cells(&self, net: usize, x: i32, z: i32) -> impl Iterator<Item = P3> {
let f = &self.nets[net];
stack_cells(x, z, f.y0, f.width)
}
fn stack_free(
&self,
net: usize,
x: i32,
z: i32,
y0: i32,
width: u8,
port_col: (i32, i32),
) -> bool {
stack_cells(x, z, y0, width)
.all(|p| self.cell_placeable_exempting(net, p, mech_at_level(p.1, y0), Some(port_col)))
}
pub fn column_free(&self, net: usize, x: i32, z: i32) -> bool {
if self.nets[net].exempt.contains(&(x, z)) {
return true;
}
if let Some(hit) = self.memo.borrow().get(&(net, x, z)) {
return *hit;
}
let free = self
.column_cells(net, x, z)
.all(|p| self.cell_placeable(net, p, self.mech_at(net, p)));
self.memo.borrow_mut().insert((net, x, z), free);
free
}
pub fn column_hugs(&self, net: usize, x: i32, z: i32) -> bool {
if self.nets[net].exempt.contains(&(x, z)) {
return false;
}
if let Some(hit) = self.hug_memo.borrow().get(&(net, x, z)) {
return *hit;
}
let hugs = self
.column_cells(net, x, z)
.any(|p| self.occ.soft_halos.contains(&p));
self.hug_memo.borrow_mut().insert((net, x, z), hugs);
hugs
}
fn mech_at(&self, net: usize, p: P3) -> Mechanism {
mech_at_level(p.1, self.nets[net].y0)
}
pub fn column_reserved(&self, net: usize, x: i32, z: i32) -> bool {
self.reserved[net].contains(&(x, z))
}
fn compute_reserved(&self, net: usize) -> BTreeSet<(i32, i32)> {
let mut out = BTreeSet::new();
for (anchor, step, width) in &self.occ.port_lanes {
let (px, py, pz) = *anchor;
if self.nets[net].exempt.contains(&(px, pz)) {
continue;
}
if step.1.abs() != 2 {
continue;
}
let y0 = if step.1 < 0 {
py + step.1 * (*width as i32 - 1)
} else {
py
};
let mut free = Vec::with_capacity(4);
for h in Heading::ALL {
let (dx, dz) = h.delta();
if self.stack_free(net, px + dx, pz + dz, y0, *width, (px, pz)) {
free.push((px + dx, pz + dz));
}
}
if free.len() != 1 {
continue;
}
let (lx, lz) = free[0];
out.insert((lx, lz));
for h in Heading::ALL {
let (dx, dz) = h.delta();
let c = (lx + dx, lz + dz);
if c == (px, pz) {
continue;
}
out.insert(c);
}
}
out
}
fn cell_placeable(&self, net: usize, p: P3, mech: Mechanism) -> bool {
self.cell_placeable_exempting(net, p, mech, None)
}
fn cell_placeable_exempting(
&self,
net: usize,
p: P3,
mech: Mechanism,
extra_exempt: Option<(i32, i32)>,
) -> bool {
if self.occ.cells.contains_key(&p) {
return false;
}
if self.occ.halos.contains_key(&p) && !self.occ.soft_halos.contains(&p) {
return false;
}
let view = OccView(self.occ);
let ours = Placement {
mech,
cell: Pos::new(p.0, p.1, p.2),
fwd: (1, 0, 0),
net: OUR_NET,
};
for q in interference_scan(p) {
if self.nets[net].exempt.contains(&(q.0, q.2)) || extra_exempt == Some((q.0, q.2)) {
continue; }
let Some((block, owner)) = self.occ.cells.get(&q) else {
continue;
};
let theirs = Placement {
mech: transport::mech_of(block),
cell: Pos::new(q.0, q.1, q.2),
fwd: transport::fwd_of(block),
net: &owner_name(owner),
};
if transport::interferes(&theirs, &ours, &view).is_some() {
return false;
}
}
if mech == Mechanism::SolidSupport && self.cuts_a_foreign_step(p) {
return false;
}
true
}
fn cuts_a_foreign_step(&self, p: P3) -> bool {
let lower = (p.0, p.1 - 1, p.2);
let Some((lb, lo)) = self.occ.cells.get(&lower) else {
return false;
};
if transport::mech_of(lb) != Mechanism::Dust {
return false;
}
let lower_owner = owner_name(lo);
[(1, 0), (-1, 0), (0, 1), (0, -1)].iter().any(|(dx, dz)| {
let upper = (p.0 + dx, p.1, p.2 + dz);
self.occ.cells.get(&upper).is_some_and(|(ub, uo)| {
transport::mech_of(ub) == Mechanism::Dust && owner_name(uo) == lower_owner
})
})
}
}
fn stack_cells(x: i32, z: i32, y0: i32, width: u8) -> impl Iterator<Item = P3> {
let lo = y0 - 1;
let hi = y0 + 2 * (width as i32 - 1);
(lo..=hi).map(move |y| (x, y, z))
}
fn mech_at_level(y: i32, y0: i32) -> Mechanism {
if (y - y0).rem_euclid(2) == 0 {
Mechanism::Dust
} else {
Mechanism::SolidSupport
}
}
fn interference_scan(p: P3) -> impl Iterator<Item = P3> {
let mut out = Vec::with_capacity(14);
out.push((p.0, p.1 + 1, p.2));
out.push((p.0, p.1 - 1, p.2));
for (dx, dz) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
for dy in [-1, 0, 1] {
out.push((p.0 + dx, p.1 + dy, p.2 + dz));
}
}
out.into_iter()
}
struct OccView<'a>(&'a OccupancyIndex);
impl BlockView for OccView<'_> {
fn block_at(&self, p: Pos) -> Option<&str> {
self.0.cells.get(&(p.x, p.y, p.z)).map(|(b, _)| b.as_str())
}
}
const OUR_NET: &str = "\0routing";
impl<'a> BusFabric<'a> {
pub fn blocker(&self, net: usize, x: i32, z: i32) -> Option<(P3, String)> {
if self.nets[net].exempt.contains(&(x, z)) {
return None;
}
for p in self.column_cells(net, x, z) {
if let Some((block, owner)) = self.occ.cells.get(&p) {
return Some((p, format!("{} `{block}`", owner_name(owner))));
}
if let Some(inst) = self.occ.halos.get(&p) {
if !self.occ.soft_halos.contains(&p) {
return Some((p, format!("the declared keepout of instance `{inst}`")));
}
}
if !self.cell_placeable(net, p, self.mech_at(net, p)) {
return Some((
p,
"foreign redstone that would interfere with the bus here".to_string(),
));
}
}
None
}
}
fn owner_name(o: &crate::design::Occupant) -> String {
match o {
crate::design::Occupant::Loose => "loose block".to_string(),
crate::design::Occupant::Instance(n) => format!("instance `{n}`"),
crate::design::Occupant::Bus(n) => format!("bus `{n}`"),
}
}
impl Fabric for BusFabric<'_> {
type Memory = Leg;
type Tag = Heading;
fn start_memory(&self) -> Leg {
Leg {
heading: Heading::Start,
run: 0,
}
}
fn moves(&self, from: &State<Leg>, ctx: &RouteCtx) -> Vec<Candidate<Leg, Heading>> {
let net = ctx.net;
let y0 = self.nets[net].y0;
let mut out = Vec::with_capacity(4);
for h in Heading::ALL {
let turning = from.mem.heading != Heading::Start && from.mem.heading != h;
if from.mem.heading != Heading::Start {
if h == from.mem.heading.opposite() {
continue;
}
if turning && from.mem.run < MIN_LEG {
continue;
}
}
let (dx, dz) = h.delta();
let to = Pos::new(from.pos.x + dx, y0, from.pos.z + dz);
let run = if turning || from.mem.heading == Heading::Start {
1
} else {
from.mem.run.saturating_add(1).min(MIN_LEG)
};
out.push(Candidate {
to: State {
pos: to,
mem: Leg { heading: h, run },
},
base_cost: 1
+ if turning { self.turn_cost } else { 0 }
+ if self.column_hugs(net, to.x, to.z) {
self.hug_cost
} else {
0
}
+ if self.column_reserved(net, to.x, to.z) {
ESCAPE_COST
} else {
0
},
tag: h,
footprint: vec![to],
});
}
out
}
fn legal(&self, _from: &State<Leg>, cand: &Candidate<Leg, Heading>, ctx: &RouteCtx) -> bool {
let p = cand.to.pos;
self.nets[ctx.net].bound.contains(p) && self.column_free(ctx.net, p.x, p.z)
}
fn budget(&self) -> Budget {
Budget::default()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct NetSpec {
pub a: P3,
pub b: P3,
pub width: u8,
}
fn form_of(spec: NetSpec, effort: Effort) -> NetForm {
let (a, b) = (spec.a, spec.b);
let mut exempt = BTreeSet::new();
exempt.insert((a.0, a.2));
exempt.insert((b.0, b.2));
let m = effort.margin;
NetForm {
y0: a.1,
width: spec.width,
exempt,
bound: Aabb::new(
Pos::new(a.0.min(b.0) - m, a.1, a.2.min(b.2) - m),
Pos::new(a.0.max(b.0) + m, a.1, a.2.max(b.2) + m),
),
}
}
fn multi_fabric<'a>(occ: &'a OccupancyIndex, specs: &[NetSpec], effort: Effort) -> BusFabric<'a> {
let nets: Vec<NetForm> = specs.iter().map(|s| form_of(*s, effort)).collect();
let n = nets.len();
let mut f = BusFabric {
occ,
nets,
turn_cost: effort.turn_cost,
memo: RefCell::new(HashMap::new()),
hug_memo: RefCell::new(HashMap::new()),
reserved: vec![BTreeSet::new(); n],
hug_cost: hug_cost(),
};
f.reserved = (0..n).map(|i| f.compute_reserved(i)).collect();
f
}
fn fabric<'a>(occ: &'a OccupancyIndex, a: P3, b: P3, width: u8, effort: Effort) -> BusFabric<'a> {
multi_fabric(occ, &[NetSpec { a, b, width }], effort)
}
pub fn search(occ: &OccupancyIndex, a: P3, b: P3, width: u8, effort: Effort) -> Option<Vec<P3>> {
if a == b || a.1 != b.1 {
return None;
}
let f = fabric(occ, a, b, width, effort);
let mut req = RouteRequest::new(Pos::new(a.0, a.1, a.2), Pos::new(b.0, b.1, b.2));
req.max_iter = effort.max_iter;
let path = route(&f, &req, &RouteCtx { net: 0 }, &|_| 0)?;
let cells: Vec<P3> = path.iter().map(|s| (s.pos.x, s.pos.y, s.pos.z)).collect();
let chain = compress(&cells);
if chain.len() < 2 {
return None;
}
for w in chain.windows(2) {
let (p, q) = (w[0], w[1]);
if p == q || (p.0 != q.0 && p.2 != q.2) {
return None;
}
}
if !self_clearance_ok(&chain) {
return None;
}
Some(chain)
}
pub fn negotiate(
occ: &OccupancyIndex,
specs: &[NetSpec],
effort: Effort,
opts: &CongestionOpts,
) -> Option<Vec<Vec<P3>>> {
if specs.len() < 2 || specs.iter().any(|s| s.a == s.b || s.a.1 != s.b.1) {
return None;
}
let f = multi_fabric(occ, specs, effort);
let reqs: Vec<NetReq> = specs
.iter()
.enumerate()
.map(|(i, s)| {
let mut req =
RouteRequest::new(Pos::new(s.a.0, s.a.1, s.a.2), Pos::new(s.b.0, s.b.1, s.b.2));
req.max_iter = effort.max_iter;
NetReq { net: i, req }
})
.collect();
let forms: Vec<NetForm> = specs.iter().map(|s| form_of(*s, effort)).collect();
let conflicts = |fp: &BTreeMap<usize, Vec<Pos>>| -> Vec<Pos> {
let mut out = Vec::new();
let items: Vec<(&usize, &Vec<Pos>)> = fp.iter().collect();
for i in 0..items.len() {
for j in (i + 1)..items.len() {
let (ni, nj) = (*items[i].0, *items[j].0);
let ((lo_i, hi_i), (lo_j, hi_j)) = (forms[ni].y_span(), forms[nj].y_span());
if hi_i < lo_j || hi_j < lo_i {
continue; }
for a in items[i].1 {
if forms[ni].exempt.contains(&(a.x, a.z)) {
continue;
}
for b in items[j].1 {
if forms[nj].exempt.contains(&(b.x, b.z)) {
continue;
}
if (a.x - b.x).abs() + (a.z - b.z).abs() <= 1 {
out.push(*a);
out.push(*b);
}
}
}
}
}
out
};
let paths = match route_all(&f, &reqs, opts, &conflicts) {
Ok(p) => p,
Err(e) => {
if std::env::var("NUCLEATION_NEGOTIATE_DEBUG").is_ok() {
eprintln!(
" negotiate: unrouted={:?} contested={:?}",
e.unrouted, e.contested
);
}
return None;
}
};
let mut chains = Vec::with_capacity(specs.len());
for i in 0..specs.len() {
let cells: Vec<P3> = paths
.get(&i)?
.iter()
.map(|s| (s.pos.x, s.pos.y, s.pos.z))
.collect();
let chain = compress(&cells);
if chain.len() < 2 || !self_clearance_ok(&chain) {
return None;
}
for w in chain.windows(2) {
let (p, q) = (w[0], w[1]);
if p == q || (p.0 != q.0 && p.2 != q.2) {
return None;
}
}
chains.push(chain);
}
Some(chains)
}
const NEGOTIATION_ROUNDS: usize = 8;
const NEGOTIATION_EFFORT: Effort = Effort {
turn_cost: 4,
margin: 48,
max_iter: 120_000,
};
pub const NEGOTIATION_GROUP_MAX: usize = 6;
pub fn negotiate_default(occ: &OccupancyIndex, specs: &[NetSpec]) -> Option<Vec<Vec<P3>>> {
negotiate(occ, specs, NEGOTIATION_EFFORT, &NEGOTIATION_OPTS)
}
const NEGOTIATION_OPTS: CongestionOpts = CongestionOpts {
max_rounds: NEGOTIATION_ROUNDS,
history_increment: 32,
present_penalty: 24,
};
fn self_clearance_ok(chain: &[P3]) -> bool {
let legs: Vec<(P3, P3)> = chain.windows(2).map(|w| (w[0], w[1])).collect();
for i in 0..legs.len() {
for j in (i + 2)..legs.len() {
if leg_distance(legs[i], legs[j]) < 2 {
return false;
}
}
}
true
}
fn leg_distance(a: (P3, P3), b: (P3, P3)) -> i32 {
let gap = |alo: i32, ahi: i32, blo: i32, bhi: i32| (blo - ahi).max(alo - bhi).max(0);
let gx = gap(
a.0 .0.min(a.1 .0),
a.0 .0.max(a.1 .0),
b.0 .0.min(b.1 .0),
b.0 .0.max(b.1 .0),
);
let gz = gap(
a.0 .2.min(a.1 .2),
a.0 .2.max(a.1 .2),
b.0 .2.min(b.1 .2),
b.0 .2.max(b.1 .2),
);
gx.max(gz)
}
fn compress(cells: &[P3]) -> Vec<P3> {
if cells.len() < 2 {
return cells.to_vec();
}
let mut out = vec![cells[0]];
for i in 1..cells.len() - 1 {
let (prev, cur, next) = (cells[i - 1], cells[i], cells[i + 1]);
let d0 = (cur.0 - prev.0, cur.2 - prev.2);
let d1 = (next.0 - cur.0, next.2 - cur.2);
if d0 != d1 {
out.push(cur);
}
}
out.push(cells[cells.len() - 1]);
out
}
pub fn clear_levels(occ: &OccupancyIndex, a: P3, b: P3, width: u8) -> Vec<i32> {
let effort = LADDER[0];
let mut clear = Vec::new();
for dy in [2, -2, 4, -4, 6, -6, 8, -8] {
let (a2, b2) = ((a.0, a.1 + dy, a.2), (b.0, b.1 + dy, b.2));
if search(occ, a2, b2, width, effort).is_some() {
clear.push(a2.1);
}
}
clear
}
fn cross_level_probe(occ: &OccupancyIndex, a: P3, b: P3, width: u8) -> String {
let mut clear = clear_levels(occ, a, b, width);
clear.sort();
if clear.is_empty() {
return " No level within 8 blocks up or down is clear either, so this is real congestion \
rather than a level-change limitation."
.to_string();
}
format!(
" A clear corridor DOES exist at y={}, and the router TRIED to hop there with a level \
shift and could not fit one — a shift needs a straight run at each end, so the pair is \
too short for the detour rather than blocked outright. Lengthen the run, or split it with \
a gate at the clear level.",
clear
.iter()
.map(|y| y.to_string())
.collect::<Vec<_>>()
.join(" or y=")
)
}
pub fn diagnose(occ: &OccupancyIndex, a: P3, b: P3, width: u8, tried: &[String]) -> String {
let effort = LADDER[LADDER.len() - 1];
let f = fabric(occ, a, b, width, effort);
for (which, anchor) in [("driver", a), ("sink", b)] {
let mut blocked = Vec::new();
let mut open = false;
for h in Heading::ALL {
let (dx, dz) = h.delta();
let (x, z) = (anchor.0 + dx, anchor.2 + dz);
match f.blocker(0, x, z) {
None => open = true,
Some((p, owner)) => blocked.push(format!("{:?} blocked by {owner}", p)),
}
}
if !open {
return format!(
"endpoint approach blocked: the {which} anchor {:?} is walled in — every \
neighbouring column of the {width}-bit stack (y {}..={}) is occupied: {}. Move \
the endpoint, shrink the neighbouring cell's keepout, or leave a clear lane \
beside the port",
anchor,
a.1 - 1,
a.1 + 2 * (width as i32 - 1),
blocked.join("; ")
);
}
}
let direct = first_blocker_on_line(&f, a, b);
let line = match direct {
Some((p, owner)) => format!("the direct line is blocked at {:?} by {owner}", p),
None => "the direct line is clear but the template shapes were rejected".to_string(),
};
let culprits = blocking_layers(&f, a, b);
let level = cross_level_probe(occ, a, b, width);
format!(
"no corridor from {:?} to {:?} for a {width}-bit bus on level y={}: {line}. A bounded \
detour search (margin {} cells, {} nodes) found no clear rectilinear corridor either — \
the layers hemming the endpoints in are: {}.{level} Move one of them, give the bus a gate \
to route through in two legs, or free a lane at least 1 cell clear of other redstone \
(dust one cell apart shorts, so the corridor needs 2 cells of pitch). Template \
attempts: {}",
a,
b,
a.1,
effort.margin,
effort.max_iter,
if culprits.is_empty() {
"none found (the bound may be too tight)".to_string()
} else {
culprits.join(", ")
},
if tried.is_empty() {
"none".to_string()
} else {
tried.join(" | ")
}
)
}
fn blocking_layers(f: &BusFabric<'_>, a: P3, b: P3) -> Vec<String> {
let mut seen = BTreeSet::new();
for anchor in [a, b] {
for dx in -2..=2i32 {
for dz in -2..=2i32 {
if let Some((_, owner)) = f.blocker(0, anchor.0 + dx, anchor.2 + dz) {
seen.insert(strip_block(&owner));
}
}
}
}
if let Some((_, owner)) = first_blocker_on_line(f, a, b) {
seen.insert(strip_block(&owner));
}
seen.into_iter().collect()
}
fn strip_block(owner: &str) -> String {
match owner.find(" `minecraft:") {
Some(i) => owner[..i].to_string(),
None => owner.to_string(),
}
}
fn first_blocker_on_line(f: &BusFabric<'_>, a: P3, b: P3) -> Option<(P3, String)> {
let sx = (b.0 - a.0).signum();
let sz = (b.2 - a.2).signum();
let mut x = a.0;
while x != b.0 {
x += sx;
if let Some(hit) = f.blocker(0, x, a.2) {
return Some(hit);
}
}
let mut z = a.2;
while z != b.2 {
z += sz;
if let Some(hit) = f.blocker(0, b.0, z) {
return Some(hit);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::design::Occupant;
fn wall(
occ: &mut OccupancyIndex,
x: i32,
zs: std::ops::RangeInclusive<i32>,
ys: std::ops::RangeInclusive<i32>,
) {
for z in zs {
for y in ys.clone() {
occ.cells
.insert((x, y, z), ("minecraft:stone".to_string(), Occupant::Loose));
}
}
}
#[test]
fn a_wall_with_a_gap_is_routed_around() {
let mut occ = OccupancyIndex::default();
wall(&mut occ, 20, -40..=13, 0..=20);
wall(&mut occ, 20, 27..=60, 0..=20);
let chain = search(&occ, (1, 2, 8), (40, 2, 8), 8, LADDER[0]).expect("corridor exists");
assert_eq!(chain[0], (1, 2, 8));
assert_eq!(*chain.last().unwrap(), (40, 2, 8));
let f = fabric(&occ, (1, 2, 8), (40, 2, 8), 8, LADDER[0]);
for w in chain.windows(2) {
let (p, q) = (w[0], w[1]);
assert!(
p.0 == q.0 || p.2 == q.2,
"leg not axis-aligned: {p:?}->{q:?}"
);
}
for c in &chain[1..chain.len() - 1] {
assert!(f.column_free(0, c.0, c.2), "corner {c:?} not free");
}
}
#[test]
fn a_sealed_wall_reports_an_actionable_reason() {
let mut occ = OccupancyIndex::default();
wall(&mut occ, 20, -400..=400, 0..=20);
assert!(search(&occ, (1, 2, 8), (40, 2, 8), 8, LADDER[0]).is_none());
let why = diagnose(&occ, (1, 2, 8), (40, 2, 8), 8, &[]);
assert!(why.contains("no corridor"), "{why}");
assert!(why.contains("(20,"), "names the blocker location: {why}");
assert!(why.contains("loose block"), "names the owner: {why}");
}
#[test]
fn a_walled_in_endpoint_says_so() {
let mut occ = OccupancyIndex::default();
for (dx, dz) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
for y in 0..=20 {
occ.cells.insert(
(1 + dx, y, 8 + dz),
("minecraft:stone".to_string(), Occupant::Loose),
);
}
}
let why = diagnose(&occ, (1, 2, 8), (40, 2, 8), 8, &[]);
assert!(why.contains("endpoint approach blocked"), "{why}");
assert!(why.contains("driver"), "{why}");
}
#[test]
fn a_self_touching_corridor_is_rejected() {
let spiral = [(0, 2, 0), (20, 2, 0), (20, 2, 10), (0, 2, 10), (0, 2, 1)];
assert!(!self_clearance_ok(&spiral));
let roomy = [(0, 2, 0), (20, 2, 0), (20, 2, 10), (0, 2, 10), (0, 2, 6)];
assert!(self_clearance_ok(&roomy));
let u = [(0, 2, 0), (20, 2, 0), (20, 2, 3), (0, 2, 3)];
assert!(self_clearance_ok(&u));
}
#[test]
fn a_corridor_keeps_clearance_from_foreign_dust() {
let mut occ = OccupancyIndex::default();
for x in 0..60 {
for k in 0..8i32 {
occ.cells.insert(
(x, 2 + 2 * k, 9),
(
"minecraft:redstone_wire[power=0]".to_string(),
Occupant::Bus("other".into()),
),
);
}
}
let f = fabric(&occ, (1, 2, 4), (40, 2, 4), 8, LADDER[0]);
assert!(!f.column_free(0, 20, 8), "z=8 hugs the foreign lane at z=9");
assert!(
!f.column_free(0, 20, 10),
"z=10 hugs it from the other side"
);
assert!(
f.column_free(0, 20, 7),
"z=7 has a clear cell of separation"
);
}
#[test]
fn a_ports_last_lane_and_its_clearance_are_reserved() {
let mut occ = OccupancyIndex::default();
for x in 24..=33 {
for z in 0..=3 {
for y in 0..=17 {
occ.cells
.insert((x, y, z), ("minecraft:stone".to_string(), Occupant::Loose));
}
}
}
occ.port_lanes.push(((24, 2, 1), (0, 2, 0), 8));
let f = fabric(&occ, (1, 2, 8), (60, 2, 8), 8, LADDER[0]);
assert!(
f.column_reserved(0, 23, 1),
"the lane itself must be reserved"
);
assert!(f.column_reserved(0, 22, 1), "the lane's -X clearance");
assert!(f.column_reserved(0, 23, 0), "the lane's -Z clearance");
assert!(f.column_reserved(0, 23, 2), "the lane's +Z clearance");
assert!(
!f.column_reserved(0, 24, 1),
"the port column must stay free"
);
assert!(!f.column_reserved(0, 10, 8));
let mut open = OccupancyIndex::default();
open.port_lanes.push(((24, 2, 1), (0, 2, 0), 8));
let g = fabric(&open, (1, 2, 8), (60, 2, 8), 8, LADDER[0]);
assert!(!g.column_reserved(0, 23, 1), "open field reserves nothing");
assert!(!g.column_reserved(0, 25, 1));
}
#[test]
fn a_narrow_gap_still_admits_a_tall_bus() {
let mut occ = OccupancyIndex::default();
for z in -300i32..=300 {
for y in 0i32..=40 {
if (z - 20).abs() <= 6 && (1..=3).contains(&y) {
continue;
}
occ.cells
.insert((20, y, z), ("minecraft:stone".to_string(), Occupant::Loose));
}
}
assert!(
search(&occ, (1, 2, 8), (40, 2, 8), 8, LADDER[1]).is_none(),
"an 8-bit stack must not squeeze through a 3-high hole"
);
assert!(search(&occ, (1, 2, 8), (40, 2, 8), 1, LADDER[1]).is_some());
}
}