feat(citygen): generator state machine, pathing helpers, terrain stage
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
383
crates/citygen/src/generate/mod.rs
Normal file
383
crates/citygen/src/generate/mod.rs
Normal file
@@ -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<StepOut, GenError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<bool>,
|
||||||
|
pub wealth: Grid<f32>,
|
||||||
|
pub decay: Grid<f32>,
|
||||||
|
pub arterial: Grid<bool>,
|
||||||
|
pub road: Grid<bool>,
|
||||||
|
pub blocks: Vec<Block>,
|
||||||
|
pub lots: Vec<Lot>,
|
||||||
|
pub lobbies: Vec<(BuildingId, Pos)>,
|
||||||
|
pub water_edge: Option<crate::geom::Dir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Feature>) -> 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<StepOut, GenError> {
|
||||||
|
Ok(StepOut::Finished)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn runner_for(stage: Stage) -> Box<dyn StageRunner> {
|
||||||
|
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<Box<dyn StageRunner>>,
|
||||||
|
error: Option<GenError>,
|
||||||
|
done: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Generator {
|
||||||
|
pub fn new(params: GenParams) -> Result<Self, GenError> {
|
||||||
|
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<City, GenError> {
|
||||||
|
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<City, GenError> {
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
crates/citygen/src/generate/terrain.rs
Normal file
135
crates/citygen/src/generate/terrain.rs
Normal file
@@ -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<OpenSimplex>,
|
||||||
|
edge: Option<Dir>,
|
||||||
|
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<StepOut, GenError> {
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
//! World model and generator for the cyberpunk city.
|
//! World model and generator for the cyberpunk city.
|
||||||
|
|
||||||
pub mod flavor;
|
pub mod flavor;
|
||||||
|
pub mod generate;
|
||||||
pub mod geom;
|
pub mod geom;
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod net;
|
pub mod net;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
|
pub mod pathing;
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod tile;
|
pub mod tile;
|
||||||
@@ -12,6 +14,7 @@ pub mod validate;
|
|||||||
pub mod world;
|
pub mod world;
|
||||||
|
|
||||||
pub use flavor::FlavorPack;
|
pub use flavor::FlavorPack;
|
||||||
|
pub use generate::{Generator, Progress, Stage};
|
||||||
pub use geom::{Dir, Grid, Pos, Rect, Size};
|
pub use geom::{Dir, Grid, Pos, Rect, Size};
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use net::*;
|
pub use net::*;
|
||||||
|
|||||||
@@ -111,13 +111,13 @@ impl GenParams {
|
|||||||
format!("{} is outside 4..=12", self.districts),
|
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(
|
return Err(bad(
|
||||||
"districts",
|
"districts",
|
||||||
format!(
|
format!(
|
||||||
"too many districts for map size ({} districts need at least {} tiles, map has {})",
|
"too many districts for map size ({} districts need at least {} tiles, map has {})",
|
||||||
self.districts,
|
self.districts,
|
||||||
self.districts as u64 * 1000,
|
self.districts as u64 * 800,
|
||||||
w as u64 * h as u64
|
w as u64 * h as u64
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|||||||
216
crates/citygen/src/pathing.rs
Normal file
216
crates/citygen/src/pathing.rs
Normal file
@@ -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<Pos> {
|
||||||
|
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<Vec<Pos>> {
|
||||||
|
let mut seen = Grid::filled(size, false);
|
||||||
|
let mut comps: Vec<Vec<Pos>> = 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<u32>,
|
||||||
|
) -> Option<Vec<Pos>> {
|
||||||
|
if !size.contains(start) || !size.contains(goal) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut g = Grid::filled(size, u32::MAX);
|
||||||
|
let mut came: Grid<Option<Pos>> = 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<Vec<Pos>> {
|
||||||
|
if !size.contains(start) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut came: Grid<Option<Pos>> = 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<Pos> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user