feat(citygen): validate cyberspace anchors, jacks and reachability
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
//! City invariants.
|
||||
|
||||
use crate::generate::connectivity::reachable_from_street;
|
||||
use crate::geom::Pos;
|
||||
use crate::geom::{Grid, Pos};
|
||||
use crate::ids::{BuildingId, DistrictId, NodeId};
|
||||
use crate::net::NodeKind;
|
||||
use crate::tile::{Feature, Terrain};
|
||||
use crate::world::{City, ConnectorKind, StratumKind};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
|
||||
/// Share of walkable tiles per stratum that must be reachable from the street.
|
||||
@@ -31,6 +32,10 @@ pub enum Violation {
|
||||
FootprintOverlap(BuildingId, BuildingId),
|
||||
NetAnchor(NodeId),
|
||||
DistrictEmpty(DistrictId),
|
||||
CyberAnchor(NodeId),
|
||||
JackUnreachable {
|
||||
pos: Pos,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for Violation {
|
||||
@@ -59,6 +64,15 @@ impl fmt::Display for Violation {
|
||||
Violation::DistrictEmpty(d) => {
|
||||
write!(f, "district {d:?} lacks a building or a public connector")
|
||||
}
|
||||
Violation::CyberAnchor(n) => {
|
||||
write!(f, "net node {n:?} is not on its cyberspace tile")
|
||||
}
|
||||
Violation::JackUnreachable { pos } => {
|
||||
write!(
|
||||
f,
|
||||
"jack at {pos:?} has no walkable neighbour or a dead port"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +85,16 @@ impl City {
|
||||
for c in &self.connectors {
|
||||
if c.kind == ConnectorKind::Jack {
|
||||
// A jack stands on a terminal tile and is used from beside it.
|
||||
let street_ok = self
|
||||
.tile(StratumKind::Street, c.pos)
|
||||
.is_some_and(|t| t.feature == Some(Feature::Terminal))
|
||||
&& c.pos
|
||||
.neighbors8()
|
||||
.any(|n| self.walkable(StratumKind::Street, n));
|
||||
let net_ok = self.walkable(StratumKind::Net, c.pos);
|
||||
if !street_ok || !net_ok {
|
||||
out.push(Violation::JackUnreachable { pos: c.pos });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for k in &c.serves {
|
||||
@@ -110,6 +134,42 @@ impl City {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(net) = self.stratum(StratumKind::Net) {
|
||||
let jacks: Vec<Pos> = self
|
||||
.connectors
|
||||
.iter()
|
||||
.filter(|c| c.kind == ConnectorKind::Jack)
|
||||
.map(|c| c.pos)
|
||||
.collect();
|
||||
let walkable = net.tiles.count(|t| t.is_walkable());
|
||||
if !jacks.is_empty() && walkable > 0 {
|
||||
let mut seen = Grid::filled(self.size, false);
|
||||
let mut queue: VecDeque<Pos> = VecDeque::new();
|
||||
for p in jacks {
|
||||
if net.tiles.get(p).is_some_and(|t| t.is_walkable()) && !seen[p] {
|
||||
seen.set(p, true);
|
||||
queue.push_back(p);
|
||||
}
|
||||
}
|
||||
while let Some(p) = queue.pop_front() {
|
||||
for n in p.neighbors4() {
|
||||
if net.tiles.get(n).is_some_and(|t| t.is_walkable()) && !seen[n] {
|
||||
seen.set(n, true);
|
||||
queue.push_back(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
let reached = seen.count(|b| *b);
|
||||
if (reached as f32) < REACH_TARGET * walkable as f32 {
|
||||
out.push(Violation::StratumUnreachable {
|
||||
stratum: StratumKind::Net,
|
||||
reached,
|
||||
walkable,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for st in &self.strata {
|
||||
for (p, t) in st.tiles.iter() {
|
||||
let bad_d = t
|
||||
@@ -156,6 +216,17 @@ impl City {
|
||||
}
|
||||
}
|
||||
|
||||
if self.stratum(StratumKind::Net).is_some() {
|
||||
for n in &self.net.nodes {
|
||||
let ok = self
|
||||
.tile(StratumKind::Net, n.cyber)
|
||||
.is_some_and(|t| t.feature == Some(Feature::Node(n.kind)));
|
||||
if !ok {
|
||||
out.push(Violation::CyberAnchor(n.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for link in &self.net.links {
|
||||
for id in [link.0, link.1] {
|
||||
if id.index() >= self.net.nodes.len() {
|
||||
@@ -168,6 +239,7 @@ impl City {
|
||||
let has_building = self.buildings.iter().any(|b| b.district == d.id);
|
||||
let has_public = self.connectors.iter().any(|c| {
|
||||
c.public
|
||||
&& c.kind != ConnectorKind::Jack
|
||||
&& self
|
||||
.tile(StratumKind::Street, c.pos)
|
||||
.is_some_and(|t| t.district == Some(d.id))
|
||||
@@ -282,4 +354,111 @@ mod tests {
|
||||
);
|
||||
assert!(format!("{}", errs[0]).len() > 5);
|
||||
}
|
||||
|
||||
fn generated() -> City {
|
||||
crate::generate::Generator::run(GenParams {
|
||||
seed: 6,
|
||||
size: SizePreset::Custom { w: 120, h: 60 },
|
||||
districts: 5,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_moved_cyber_nodes_and_walled_in_jacks() {
|
||||
let mut c = generated();
|
||||
assert!(c.validate().is_ok());
|
||||
let moved = c.net.nodes[0].cyber;
|
||||
c.net.nodes[0].cyber = Pos::new(moved.x, (moved.y + 1) % c.size.h as i32);
|
||||
let v = c.validate().unwrap_err();
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|v| matches!(v, Violation::CyberAnchor(NodeId(0)))),
|
||||
"{v:?}"
|
||||
);
|
||||
c.net.nodes[0].cyber = moved;
|
||||
|
||||
let jack = c
|
||||
.connectors
|
||||
.iter()
|
||||
.find(|k| k.kind == ConnectorKind::Jack)
|
||||
.unwrap()
|
||||
.pos;
|
||||
for n in jack.neighbors8() {
|
||||
if let Some(t) = c.tile_mut(StratumKind::Street, n) {
|
||||
t.terrain = Terrain::Wall;
|
||||
}
|
||||
}
|
||||
let v = c.validate().unwrap_err();
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|v| matches!(v, Violation::JackUnreachable { pos } if *pos == jack)),
|
||||
"{v:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn street_district(c: &City, p: Pos) -> Option<DistrictId> {
|
||||
c.tile(StratumKind::Street, p).and_then(|t| t.district)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jack_is_not_a_public_connector_for_districts() {
|
||||
let mut c = generated();
|
||||
// Strip every non-jack public connector from district 0: it must now be flagged.
|
||||
let d = DistrictId(0);
|
||||
let keep: Vec<bool> = c
|
||||
.connectors
|
||||
.iter()
|
||||
.map(|k| {
|
||||
k.kind == ConnectorKind::Jack || !k.public || street_district(&c, k.pos) != Some(d)
|
||||
})
|
||||
.collect();
|
||||
let mut i = 0;
|
||||
c.connectors.retain(|_| {
|
||||
let k = keep[i];
|
||||
i += 1;
|
||||
k
|
||||
});
|
||||
let v = c.validate().unwrap_err();
|
||||
assert!(
|
||||
v.iter()
|
||||
.any(|v| matches!(v, Violation::DistrictEmpty(x) if *x == d)),
|
||||
"{v:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn net_reachability_is_checked_from_the_jacks() {
|
||||
let mut c = generated();
|
||||
// Ring every port with wall: the grid stays walkable but nothing reaches it.
|
||||
let jacks: Vec<Pos> = c
|
||||
.connectors
|
||||
.iter()
|
||||
.filter(|k| k.kind == ConnectorKind::Jack)
|
||||
.map(|k| k.pos)
|
||||
.collect();
|
||||
let net = c.stratum_mut(StratumKind::Net).unwrap();
|
||||
for j in &jacks {
|
||||
for n in j.neighbors8() {
|
||||
if let Some(t) = net.tiles.get_mut(n)
|
||||
&& !jacks.contains(&n)
|
||||
{
|
||||
t.terrain = Terrain::NetWall;
|
||||
t.feature = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let v = c.validate().unwrap_err();
|
||||
assert!(
|
||||
v.iter().any(|v| matches!(
|
||||
v,
|
||||
Violation::StratumUnreachable {
|
||||
stratum: StratumKind::Net,
|
||||
..
|
||||
}
|
||||
)),
|
||||
"{v:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user