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.
|
//! City invariants.
|
||||||
|
|
||||||
use crate::generate::connectivity::reachable_from_street;
|
use crate::generate::connectivity::reachable_from_street;
|
||||||
use crate::geom::Pos;
|
use crate::geom::{Grid, Pos};
|
||||||
use crate::ids::{BuildingId, DistrictId, NodeId};
|
use crate::ids::{BuildingId, DistrictId, NodeId};
|
||||||
use crate::net::NodeKind;
|
use crate::net::NodeKind;
|
||||||
use crate::tile::{Feature, Terrain};
|
use crate::tile::{Feature, Terrain};
|
||||||
use crate::world::{City, ConnectorKind, StratumKind};
|
use crate::world::{City, ConnectorKind, StratumKind};
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// Share of walkable tiles per stratum that must be reachable from the street.
|
/// Share of walkable tiles per stratum that must be reachable from the street.
|
||||||
@@ -31,6 +32,10 @@ pub enum Violation {
|
|||||||
FootprintOverlap(BuildingId, BuildingId),
|
FootprintOverlap(BuildingId, BuildingId),
|
||||||
NetAnchor(NodeId),
|
NetAnchor(NodeId),
|
||||||
DistrictEmpty(DistrictId),
|
DistrictEmpty(DistrictId),
|
||||||
|
CyberAnchor(NodeId),
|
||||||
|
JackUnreachable {
|
||||||
|
pos: Pos,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Violation {
|
impl fmt::Display for Violation {
|
||||||
@@ -59,6 +64,15 @@ impl fmt::Display for Violation {
|
|||||||
Violation::DistrictEmpty(d) => {
|
Violation::DistrictEmpty(d) => {
|
||||||
write!(f, "district {d:?} lacks a building or a public connector")
|
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 {
|
for c in &self.connectors {
|
||||||
if c.kind == ConnectorKind::Jack {
|
if c.kind == ConnectorKind::Jack {
|
||||||
// A jack stands on a terminal tile and is used from beside it.
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
for k in &c.serves {
|
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 st in &self.strata {
|
||||||
for (p, t) in st.tiles.iter() {
|
for (p, t) in st.tiles.iter() {
|
||||||
let bad_d = t
|
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 link in &self.net.links {
|
||||||
for id in [link.0, link.1] {
|
for id in [link.0, link.1] {
|
||||||
if id.index() >= self.net.nodes.len() {
|
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_building = self.buildings.iter().any(|b| b.district == d.id);
|
||||||
let has_public = self.connectors.iter().any(|c| {
|
let has_public = self.connectors.iter().any(|c| {
|
||||||
c.public
|
c.public
|
||||||
|
&& c.kind != ConnectorKind::Jack
|
||||||
&& self
|
&& self
|
||||||
.tile(StratumKind::Street, c.pos)
|
.tile(StratumKind::Street, c.pos)
|
||||||
.is_some_and(|t| t.district == Some(d.id))
|
.is_some_and(|t| t.district == Some(d.id))
|
||||||
@@ -282,4 +354,111 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(format!("{}", errs[0]).len() > 5);
|
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