diff --git a/crates/citygen/src/generate/mod.rs b/crates/citygen/src/generate/mod.rs new file mode 100644 index 0000000..03ebe6b --- /dev/null +++ b/crates/citygen/src/generate/mod.rs @@ -0,0 +1,383 @@ +//! The resumable generator: eleven chunked stages over a growing `City`. + +pub mod terrain; + +use crate::flavor::FlavorPack; +use crate::geom::{Grid, Pos, Rect, Size}; +use crate::ids::{BuildingId, DistrictId}; +use crate::params::{GenError, GenParams}; +use crate::rng::stage_rng; +use crate::tile::{Feature, Terrain, Tile}; +use crate::world::{BuildingKind, City, HeightClass, StratumKind}; +use rand_chacha::ChaCha8Rng; + +/// Generation stages, in execution order. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Stage { + Terrain, + Districts, + Arterials, + Streets, + Lots, + Verticals, + Interiors, + Connectivity, + Dressing, + Naming, + Net, +} + +impl Stage { + pub const ALL: [Stage; 11] = [ + Stage::Terrain, + Stage::Districts, + Stage::Arterials, + Stage::Streets, + Stage::Lots, + Stage::Verticals, + Stage::Interiors, + Stage::Connectivity, + Stage::Dressing, + Stage::Naming, + Stage::Net, + ]; + + pub fn label(self) -> &'static str { + match self { + Stage::Terrain => "Shaping terrain", + Stage::Districts => "Zoning districts", + Stage::Arterials => "Routing arterials", + Stage::Streets => "Carving streets", + Stage::Lots => "Raising buildings", + Stage::Verticals => "Stacking strata", + Stage::Interiors => "Fitting interiors", + Stage::Connectivity => "Linking levels", + Stage::Dressing => "Hanging neon", + Stage::Naming => "Naming places", + Stage::Net => "Wiring the Net", + } + } + + pub fn index(self) -> u8 { + Stage::ALL.iter().position(|s| *s == self).unwrap_or(0) as u8 + } +} + +/// What `Generator::step` reports. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Progress { + Running { stage: Stage, pct: f32 }, + Done, +} + +/// Output of one `StageRunner::step`. +pub(crate) enum StepOut { + Continue(f32), + Finished, +} + +/// One stage, run as many small steps. +pub(crate) trait StageRunner { + fn begin(&mut self, ctx: &mut GenCtx) -> Result<(), GenError>; + fn step(&mut self, ctx: &mut GenCtx) -> Result; +} + +// Scratch types are populated by stages that land in later tasks; the allows go when they do. +/// A rectangular block between streets, owned by one district. +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct Block { + pub rect: Rect, + pub district: DistrictId, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub(crate) enum LotUse { + Building { + kind: BuildingKind, + class: HeightClass, + basement: bool, + }, + Plaza, + Parking, +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct Lot { + pub rect: Rect, + pub district: DistrictId, + pub use_: LotUse, +} + +/// Intermediate state shared between stages; never serialized. +#[allow(dead_code)] +pub(crate) struct Scratch { + pub land: Grid, + pub wealth: Grid, + pub decay: Grid, + pub arterial: Grid, + pub road: Grid, + pub blocks: Vec, + pub lots: Vec, + pub lobbies: Vec<(BuildingId, Pos)>, + pub water_edge: Option, +} + +impl Scratch { + fn new(size: Size) -> Self { + Scratch { + land: Grid::filled(size, true), + wealth: Grid::filled(size, 0.5), + decay: Grid::filled(size, 0.5), + arterial: Grid::filled(size, false), + road: Grid::filled(size, false), + blocks: Vec::new(), + lots: Vec::new(), + lobbies: Vec::new(), + water_edge: None, + } + } +} + +/// Everything a stage can see and mutate. +#[allow(dead_code)] +pub(crate) struct GenCtx { + pub city: City, + pub flavor: FlavorPack, + pub seed: u64, + pub scratch: Scratch, +} + +#[allow(dead_code)] +impl GenCtx { + pub fn rng(&self, stage: Stage) -> ChaCha8Rng { + stage_rng(self.seed, stage.index()) + } + + pub fn size(&self) -> Size { + self.city.size + } + + pub fn params(&self) -> &GenParams { + &self.city.params + } + + pub fn terrain(&self, k: StratumKind, p: Pos) -> Terrain { + self.city + .tile(k, p) + .map(|t| t.terrain) + .unwrap_or(Terrain::Void) + } + + pub fn set_terrain(&mut self, k: StratumKind, p: Pos, t: Terrain) -> bool { + match self.city.tile_mut(k, p) { + Some(tile) => { + tile.terrain = t; + true + } + None => false, + } + } + + pub fn set_feature(&mut self, k: StratumKind, p: Pos, f: Option) -> bool { + match self.city.tile_mut(k, p) { + Some(tile) => { + tile.feature = f; + true + } + None => false, + } + } + + pub fn tile(&self, k: StratumKind, p: Pos) -> Option<&Tile> { + self.city.tile(k, p) + } + + pub fn in_district(&self, p: Pos, id: DistrictId) -> bool { + self.city + .tile(StratumKind::Street, p) + .is_some_and(|t| t.district == Some(id)) + } + + pub fn is_land(&self, p: Pos) -> bool { + self.scratch.land.get(p).copied().unwrap_or(false) + } +} + +/// A stage that does nothing; placeholder until the real stage lands. +struct NoopStage; + +impl StageRunner for NoopStage { + fn begin(&mut self, _ctx: &mut GenCtx) -> Result<(), GenError> { + Ok(()) + } + fn step(&mut self, _ctx: &mut GenCtx) -> Result { + Ok(StepOut::Finished) + } +} + +pub(crate) fn runner_for(stage: Stage) -> Box { + match stage { + Stage::Terrain => Box::new(terrain::TerrainStage::default()), + _ => Box::new(NoopStage), + } +} + +/// Drives the stages one small step at a time so a UI can draw between steps. +pub struct Generator { + ctx: GenCtx, + stage_idx: usize, + runner: Option>, + error: Option, + done: bool, +} + +impl Generator { + pub fn new(params: GenParams) -> Result { + params.validate()?; + let flavor = FlavorPack::load(¶ms.flavor)?; + let city = City::empty(params.clone()); + let scratch = Scratch::new(city.size); + Ok(Generator { + ctx: GenCtx { + seed: params.seed, + city, + flavor, + scratch, + }, + stage_idx: 0, + runner: None, + error: None, + done: false, + }) + } + + /// Runs one small unit of work. + pub fn step(&mut self) -> Progress { + if self.done || self.error.is_some() { + return Progress::Done; + } + let stage = Stage::ALL[self.stage_idx]; + if self.runner.is_none() { + let mut r = runner_for(stage); + if let Err(e) = r.begin(&mut self.ctx) { + self.error = Some(e); + return Progress::Done; + } + self.runner = Some(r); + } + let out = self + .runner + .as_mut() + .expect("runner set above") + .step(&mut self.ctx); + match out { + Ok(StepOut::Continue(pct)) => Progress::Running { + stage, + pct: pct.clamp(0.0, 1.0), + }, + Ok(StepOut::Finished) => { + self.runner = None; + self.stage_idx += 1; + if self.stage_idx == Stage::ALL.len() { + self.done = true; + Progress::Done + } else { + Progress::Running { + stage: Stage::ALL[self.stage_idx], + pct: 0.0, + } + } + } + Err(e) => { + self.error = Some(e); + Progress::Done + } + } + } + + /// The city so far; partial while running. + pub fn city(&self) -> &City { + &self.ctx.city + } + + pub fn error(&self) -> Option<&GenError> { + self.error.as_ref() + } + + pub fn is_done(&self) -> bool { + self.done || self.error.is_some() + } + + /// Validates and returns the finished city. + pub fn finish(self) -> Result { + if let Some(e) = self.error { + return Err(e); + } + self.ctx.city.validate().map_err(GenError::Invalid)?; + Ok(self.ctx.city) + } + + /// Runs every stage to completion. + pub fn run(params: GenParams) -> Result { + let mut g = Generator::new(params)?; + while let Progress::Running { .. } = g.step() {} + g.finish() + } + + /// Test helper: run until `stage` has finished and hand back the context. + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn run_until(params: GenParams, stage: Stage) -> GenCtx { + let mut g = Generator::new(params).expect("valid params"); + loop { + match g.step() { + Progress::Running { stage: s, .. } if s.index() > stage.index() => break, + Progress::Running { .. } => {} + Progress::Done => break, + } + } + assert!(g.error.is_none(), "{:?}", g.error); + g.ctx + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::params::{GenParams, SizePreset}; + + #[test] + fn generator_walks_all_stages_in_order() { + let mut g = Generator::new(GenParams { + seed: 1, + size: SizePreset::Custom { w: 80, h: 40 }, + districts: 4, + ..Default::default() + }) + .unwrap(); + let mut seen = vec![]; + while let Progress::Running { stage, pct } = g.step() { + assert!((0.0..=1.0).contains(&pct)); + if seen.last() != Some(&stage) { + seen.push(stage); + } + } + assert_eq!(seen, Stage::ALL.to_vec()); + assert!(g.error().is_none()); + assert!(g.finish().is_ok()); + } + + #[test] + fn generator_rejects_invalid_params() { + assert!( + Generator::new(GenParams { + districts: 99, + ..Default::default() + }) + .is_err() + ); + } +} diff --git a/crates/citygen/src/generate/terrain.rs b/crates/citygen/src/generate/terrain.rs new file mode 100644 index 0000000..280a752 --- /dev/null +++ b/crates/citygen/src/generate/terrain.rs @@ -0,0 +1,135 @@ +//! Stage 1: water and land. + +use super::{GenCtx, Stage, StageRunner, StepOut}; +use crate::geom::{Dir, Pos}; +use crate::params::{GenError, WaterMode}; +use crate::tile::Terrain; +use crate::world::StratumKind; +use noise::{NoiseFn, OpenSimplex}; +use rand::Rng; +use rand::seq::IndexedRandom; + +#[derive(Default)] +pub(crate) struct TerrainStage { + row: i32, + noise: Option, + edge: Option, + river_axis_horizontal: bool, + river_offset: f64, +} + +impl TerrainStage { + fn is_water(&self, ctx: &GenCtx, p: Pos) -> bool { + let size = ctx.size(); + let (w, h) = (size.w as f64, size.h as f64); + let noise = self.noise.as_ref().expect("begin sets noise"); + match ctx.params().water { + WaterMode::None => false, + WaterMode::Bay => { + let edge = self.edge.expect("begin sets edge"); + // `t` runs along the edge, `d` is the distance from it. + let (t, d, extent) = match edge { + Dir::N => (p.x as f64, p.y as f64, h), + Dir::S => (p.x as f64, h - 1.0 - p.y as f64, h), + Dir::W => (p.y as f64, p.x as f64, w), + _ => (p.y as f64, w - 1.0 - p.x as f64, w), + }; + let depth = 0.15 * extent + noise.get([t * 0.04, 7.3]) * 0.06 * extent; + d < depth + } + WaterMode::River => { + let (t, c, extent) = if self.river_axis_horizontal { + (p.x as f64, p.y as f64, h) + } else { + (p.y as f64, p.x as f64, w) + }; + let centre = + extent / 2.0 + self.river_offset + noise.get([t * 0.02, 3.1]) * 0.25 * extent; + let half = 3.0 + (noise.get([t * 0.05, 9.7]) + 1.0); + (c - centre).abs() < half + } + } + } +} + +impl StageRunner for TerrainStage { + fn begin(&mut self, ctx: &mut GenCtx) -> Result<(), GenError> { + let mut rng = ctx.rng(Stage::Terrain); + self.noise = Some(OpenSimplex::new(ctx.seed as u32)); + self.edge = Dir::CARDINAL.choose(&mut rng).copied(); + self.river_axis_horizontal = rng.random_bool(0.5); + self.river_offset = rng.random_range(-0.1..0.1) * ctx.size().h as f64; + ctx.scratch.water_edge = self.edge; + self.row = 0; + Ok(()) + } + + fn step(&mut self, ctx: &mut GenCtx) -> Result { + let size = ctx.size(); + if self.row >= size.h as i32 { + return Ok(StepOut::Finished); + } + let y = self.row; + for x in 0..size.w as i32 { + let p = Pos::new(x, y); + if self.is_water(ctx, p) { + ctx.scratch.land.set(p, false); + ctx.set_terrain(StratumKind::Street, p, Terrain::Water); + } + } + self.row += 1; + Ok(StepOut::Continue(self.row as f32 / size.h as f32)) + } +} + +#[cfg(test)] +mod tests { + use crate::generate::Generator; + use crate::params::{GenParams, SizePreset, WaterMode}; + use crate::tile::Terrain; + use crate::world::StratumKind; + + #[test] + fn bay_covers_ten_to_twenty_percent_of_street() { + for seed in 0..5 { + let c = Generator::run(GenParams { + seed, + water: WaterMode::Bay, + size: SizePreset::Custom { w: 120, h: 60 }, + districts: 4, + ..Default::default() + }) + .unwrap(); + let st = c.stratum(StratumKind::Street).unwrap(); + let water = st.tiles.count(|t| t.terrain == Terrain::Water) as f32 + / st.tiles.size().area() as f32; + assert!((0.08..=0.25).contains(&water), "seed {seed}: {water}"); + } + let c = Generator::run(GenParams { + seed: 3, + water: WaterMode::None, + size: SizePreset::Custom { w: 120, h: 60 }, + districts: 4, + ..Default::default() + }) + .unwrap(); + assert_eq!( + c.stratum(StratumKind::Street) + .unwrap() + .tiles + .count(|t| t.terrain == Terrain::Water), + 0 + ); + let c = Generator::run(GenParams { + seed: 3, + water: WaterMode::River, + size: SizePreset::Custom { w: 120, h: 60 }, + districts: 4, + ..Default::default() + }) + .unwrap(); + let st = c.stratum(StratumKind::Street).unwrap(); + let water = st.tiles.count(|t| t.terrain == Terrain::Water); + assert!(water > 300 && water < 1500, "river tiles {water}"); + } +} diff --git a/crates/citygen/src/lib.rs b/crates/citygen/src/lib.rs index 93893fd..335bc70 100644 --- a/crates/citygen/src/lib.rs +++ b/crates/citygen/src/lib.rs @@ -1,10 +1,12 @@ //! World model and generator for the cyberpunk city. pub mod flavor; +pub mod generate; pub mod geom; pub mod ids; pub mod net; pub mod params; +pub mod pathing; pub mod rng; pub mod theme; pub mod tile; @@ -12,6 +14,7 @@ pub mod validate; pub mod world; pub use flavor::FlavorPack; +pub use generate::{Generator, Progress, Stage}; pub use geom::{Dir, Grid, Pos, Rect, Size}; pub use ids::*; pub use net::*; diff --git a/crates/citygen/src/params.rs b/crates/citygen/src/params.rs index 886c367..59fb082 100644 --- a/crates/citygen/src/params.rs +++ b/crates/citygen/src/params.rs @@ -111,13 +111,13 @@ impl GenParams { format!("{} is outside 4..=12", self.districts), )); } - if (w as u64) * (h as u64) < self.districts as u64 * 1000 { + if (w as u64) * (h as u64) < self.districts as u64 * 800 { return Err(bad( "districts", format!( "too many districts for map size ({} districts need at least {} tiles, map has {})", self.districts, - self.districts as u64 * 1000, + self.districts as u64 * 800, w as u64 * h as u64 ), )); diff --git a/crates/citygen/src/pathing.rs b/crates/citygen/src/pathing.rs new file mode 100644 index 0000000..22e2ea3 --- /dev/null +++ b/crates/citygen/src/pathing.rs @@ -0,0 +1,216 @@ +//! Grid search helpers shared by generation stages: flood fill, components, A*, BFS, Bresenham. + +use crate::geom::{Grid, Pos, Size}; +use std::cmp::Reverse; +use std::collections::{BinaryHeap, VecDeque}; + +/// All positions 4-connected to `start` through `passable`. Empty if `start` is not passable. +pub fn flood_fill(size: Size, start: Pos, passable: impl Fn(Pos) -> bool) -> Vec { + let mut seen = Grid::filled(size, false); + let mut out = Vec::new(); + if !size.contains(start) || !passable(start) { + return out; + } + let mut queue = VecDeque::from([start]); + seen.set(start, true); + while let Some(p) = queue.pop_front() { + out.push(p); + for n in p.neighbors4() { + if size.contains(n) && !seen[n] && passable(n) { + seen.set(n, true); + queue.push_back(n); + } + } + } + out +} + +/// Every 4-connected component of passable tiles, largest first (ties: earliest first cell). +pub fn components(size: Size, passable: impl Fn(Pos) -> bool) -> Vec> { + let mut seen = Grid::filled(size, false); + let mut comps: Vec> = Vec::new(); + for y in 0..size.h as i32 { + for x in 0..size.w as i32 { + let start = Pos::new(x, y); + if seen[start] || !passable(start) { + continue; + } + let mut comp = Vec::new(); + let mut queue = VecDeque::from([start]); + seen.set(start, true); + while let Some(p) = queue.pop_front() { + comp.push(p); + for n in p.neighbors4() { + if size.contains(n) && !seen[n] && passable(n) { + seen.set(n, true); + queue.push_back(n); + } + } + } + comps.push(comp); + } + } + comps.sort_by_key(|c| Reverse(c.len())); + comps +} + +/// 4-neighbour A*. `cost(from, to)` returns `None` for impassable steps. Path includes both ends. +pub fn astar( + size: Size, + start: Pos, + goal: Pos, + cost: impl Fn(Pos, Pos) -> Option, +) -> Option> { + if !size.contains(start) || !size.contains(goal) { + return None; + } + let mut g = Grid::filled(size, u32::MAX); + let mut came: Grid> = Grid::filled(size, None); + let mut heap = BinaryHeap::new(); + let mut counter = 0u32; + g.set(start, 0); + heap.push(Reverse((start.manhattan(goal) as u32, counter, start))); + while let Some(Reverse((_, _, p))) = heap.pop() { + if p == goal { + let mut path = vec![p]; + let mut cur = p; + while let Some(prev) = came[cur] { + path.push(prev); + cur = prev; + } + path.reverse(); + return Some(path); + } + let gp = g[p]; + for n in p.neighbors4() { + if !size.contains(n) { + continue; + } + let Some(c) = cost(p, n) else { continue }; + let ng = gp.saturating_add(c); + if ng < g[n] { + g.set(n, ng); + came.set(n, Some(p)); + counter += 1; + heap.push(Reverse((ng + n.manhattan(goal) as u32, counter, n))); + } + } + } + None +} + +/// Shortest 4-path from `start` through `passable` tiles to the nearest `target` tile, +/// at most `max` steps long. The target tile itself need not be passable. +pub fn bfs_nearest( + size: Size, + start: Pos, + passable: impl Fn(Pos) -> bool, + target: impl Fn(Pos) -> bool, + max: usize, +) -> Option> { + if !size.contains(start) { + return None; + } + let mut came: Grid> = Grid::filled(size, None); + let mut dist = Grid::filled(size, usize::MAX); + let mut queue = VecDeque::from([start]); + dist.set(start, 0); + while let Some(p) = queue.pop_front() { + let d = dist[p]; + if target(p) && p != start { + let mut path = vec![p]; + let mut cur = p; + while let Some(prev) = came[cur] { + path.push(prev); + cur = prev; + } + path.reverse(); + return Some(path); + } + if d >= max { + continue; + } + for n in p.neighbors4() { + if size.contains(n) && dist[n] == usize::MAX && (passable(n) || target(n)) { + dist.set(n, d + 1); + came.set(n, Some(p)); + queue.push_back(n); + } + } + } + None +} + +/// Integer line from `a` to `b`, both inclusive. +pub fn bresenham(a: Pos, b: Pos) -> Vec { + let mut out = Vec::new(); + let (mut x, mut y) = (a.x, a.y); + let dx = (b.x - a.x).abs(); + let dy = -(b.y - a.y).abs(); + let sx = if a.x < b.x { 1 } else { -1 }; + let sy = if a.y < b.y { 1 } else { -1 }; + let mut err = dx + dy; + loop { + out.push(Pos::new(x, y)); + if x == b.x && y == b.y { + break; + } + let e2 = 2 * err; + if e2 >= dy { + err += dy; + x += sx; + } + if e2 <= dx { + err += dx; + y += sy; + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geom::{Pos, Size}; + + #[test] + fn astar_routes_around_block() { + let size = Size { w: 5, h: 5 }; + let wall = |p: Pos| p.x == 2 && p.y != 4; + let path = astar(size, Pos::new(0, 0), Pos::new(4, 0), |_, to| { + if wall(to) { None } else { Some(1) } + }) + .unwrap(); + assert_eq!(path[0], Pos::new(0, 0)); + assert_eq!(*path.last().unwrap(), Pos::new(4, 0)); + assert!(path.iter().all(|p| !wall(*p))); + assert_eq!(path.len(), 13); + } + + #[test] + fn components_are_largest_first() { + let size = Size { w: 6, h: 1 }; + let pass = |p: Pos| p.x != 2; + let c = components(size, pass); + assert_eq!(c.len(), 2); + assert_eq!(c[0].len(), 3); + assert_eq!(c[1].len(), 2); + assert_eq!(flood_fill(size, Pos::new(0, 0), pass).len(), 2); + } + + #[test] + fn bfs_nearest_finds_target() { + let size = Size { w: 10, h: 1 }; + let p = bfs_nearest(size, Pos::new(0, 0), |_| true, |p| p.x == 7, 20).unwrap(); + assert_eq!(p.len(), 8); + assert!(bfs_nearest(size, Pos::new(0, 0), |_| true, |p| p.x == 7, 3).is_none()); + } + + #[test] + fn bresenham_endpoints() { + let line = bresenham(Pos::new(0, 0), Pos::new(3, 1)); + assert_eq!(line.first(), Some(&Pos::new(0, 0))); + assert_eq!(line.last(), Some(&Pos::new(3, 1))); + assert_eq!(line.len(), 4); + } +}