//! Capital cities: where you walk when you land somewhere people live. //! //! A capital comes at two scales. Outside is the whole city, seen from //! high up: a wall around it, a landing field beyond the west gate, avenues //! to a market square, and streets and alleys between blocks of buildings, //! each one a small roof with a door. Bump a door and you go in, and the //! building becomes a map of its own at a much closer scale: rooms, //! furniture, and the people who live or work there. //! //! - `plan` lays out the city: walls, gates, quarters, streets, landmarks //! and the buildings, and which of them are shops. //! - `interior` builds what's inside a building when you go in. //! - `words` holds what everyone says, and what things are called. //! //! A city is generated from its place, so it's the same city every time you //! land, and the more people live on a world the bigger its capital is. //! Interiors come from the building, so a house has the same rooms each //! visit too. //! //! The world it stands on shapes it as well (see `Land`): the ground, what //! grows in its parks and what stands outside its walls, how many parks //! there are, and, on an ocean world, the sea all round. The weather on the //! day you land only changes how it looks. mod interior; mod plan; pub mod words; pub use interior::{Interior, Purpose, Thing}; use rand::seq::IndexedRandom; use crate::dungeon::Pos; use crate::names::Culture; use crate::seed; use crate::space::survey::{self, FREEZING, Temperature}; use crate::space::system::{PlaceKind, System, Terrain}; /// Columns and rows your ship takes up on the pad. pub const SHIP_SIZE: (i32, i32) = (5, 3); #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Tile { /// Outside the walls. Ground, /// The landing field. Pad, /// Your ship, standing on the pad. Ship, CityWall, Gate, Street, /// Paving: the market square, plazas, the forecourts of great buildings. Square, /// Open ground inside the walls: yards, courtyards, parks. Garden, /// A pond. Water, /// The open sea round a city on an ocean world. Sea, /// Pipes, scrap: in the way, just for looks. Cultures that don't leave /// things lying about plant trees instead. Decor, /// A tree, or whatever grows instead on this world. Tree, /// Something outside the walls: a boulder, a cactus, an ice spire, a /// vent. Each world has two sorts (see `Town::wild_sort`). Wild, /// The well, fountain or furnace in the middle of the market square. Centrepiece, /// A market stall's awning. Stall, Statue, Well, Lamp, /// A building, seen from the street. Which one is in `Town::building_at`. Roof, /// A building's street door. Bump it to go in, unless it's barred. Door { open: bool, }, /// A building's walls, from inside. Wall, Floor, /// The way between two rooms. Doorway, /// The door back out to the street. Exit, /// A shop's counter, with the keeper behind it. Counter, /// The shop's wares, behind the counter. Shelf, Rug, Furniture(Thing), } impl Tile { pub fn walkable(self) -> bool { match self { Tile::Ground | Tile::Pad | Tile::Gate | Tile::Street | Tile::Square | Tile::Garden | Tile::Floor | Tile::Doorway | Tile::Rug => true, Tile::Furniture(thing) => thing.walkable(), _ => false, } } /// Open air inside the walls, where townsfolk wander. pub fn outdoors(self) -> bool { matches!(self, Tile::Gate | Tile::Street | Tile::Square | Tile::Garden) } /// A street or square: what a building's door has to open onto. fn paved(self) -> bool { matches!(self, Tile::Street | Tile::Square) } } /// A rectangle of cells, edges included. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Rect { left: i32, top: i32, right: i32, bottom: i32, } impl Rect { const fn new(left: i32, top: i32, right: i32, bottom: i32) -> Rect { Rect { left, top, right, bottom } } fn width(self) -> i32 { self.right - self.left + 1 } fn height(self) -> i32 { self.bottom - self.top + 1 } fn area(self) -> i32 { self.width().max(0) * self.height().max(0) } fn centre(self) -> Pos { Pos::new((self.left + self.right) / 2, (self.top + self.bottom) / 2) } fn contains(self, p: Pos) -> bool { p.x >= self.left && p.x <= self.right && p.y >= self.top && p.y <= self.bottom } fn cells(self) -> impl Iterator { (self.top..=self.bottom) .flat_map(move |y| (self.left..=self.right).map(move |x| Pos::new(x, y))) } } /// One of the four ways a door can face. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Side { North, South, West, East, } impl Side { pub fn step(self) -> (i32, i32) { match self { Side::North => (0, -1), Side::South => (0, 1), Side::West => (-1, 0), Side::East => (1, 0), } } fn opposite(self) -> Side { match self { Side::North => Side::South, Side::South => Side::North, Side::West => Side::East, Side::East => Side::West, } } } /// A map you can walk around, and the people on it. The city's streets are /// one; each building's inside is another. pub struct Site { pub width: i32, pub height: i32, tiles: Vec, pub people: Vec, /// What's off the edge of the map. edge: Tile, /// Whether people here keep to the open air (the streets) or to the /// floors (indoors). streets: bool, } impl Site { fn new(width: i32, height: i32, fill: Tile, edge: Tile, streets: bool) -> Site { Site { width, height, tiles: vec![fill; (width * height) as usize], people: Vec::new(), edge, streets, } } pub fn contains(&self, p: Pos) -> bool { p.x >= 0 && p.y >= 0 && p.x < self.width && p.y < self.height } fn index(&self, p: Pos) -> usize { (p.y * self.width + p.x) as usize } /// The tile at `p`; off the map is the edge, so nothing walks off it. pub fn tile(&self, p: Pos) -> Tile { if self.contains(p) { self.tiles[self.index(p)] } else { self.edge } } fn set(&mut self, p: Pos, tile: Tile) { let i = self.index(p); self.tiles[i] = tile; } fn fill(&mut self, r: Rect, tile: Tile) { for p in r.cells() { if self.contains(p) { self.set(p, tile); } } } pub fn positions(&self) -> impl Iterator + use<> { Rect::new(0, 0, self.width - 1, self.height - 1).cells() } pub fn person_at(&self, p: Pos) -> Option { self.people.iter().position(|person| person.pos == p) } /// Whether someone could step onto `p` right now. pub fn free(&self, p: Pos) -> bool { self.tile(p).walkable() && self.person_at(p).is_none() } /// Where people here may wander. fn roams(&self, tile: Tile) -> bool { if self.streets { tile.outdoors() } else { tile.walkable() } } /// Steps someone who's wandering aside onto `to`, where you were, as you /// walk through them, if they'd go there. Otherwise they stay where /// they are, and you share the cell until they wander off. pub fn make_way(&mut self, who: usize, to: Pos) { if !self.people[who].stays && self.roams(self.tile(to)) && self.free(to) { self.people[who].pos = to; } } /// Moves someone a step at random, if they're the wandering kind, /// staying off `avoid` (where you're standing). pub fn wander(&mut self, who: usize, rng: &mut seed::Rng, avoid: Pos) { if self.people[who].stays { return; } let from = self.people[who].pos; let steps: Vec = from .neighbours() .filter(|&p| p != avoid && self.roams(self.tile(p)) && self.free(p)) .collect(); if let Some(&to) = steps.choose(rng) { self.people[who].pos = to; } } /// Every cell you can walk to from `from`. pub fn reachable(&self, from: Pos) -> Vec { let mut seen = vec![false; self.tiles.len()]; if !self.contains(from) { return seen; } let mut queue = std::collections::VecDeque::from([from]); seen[self.index(from)] = true; while let Some(p) = queue.pop_front() { for n in p.neighbours() { if self.contains(n) && self.tile(n).walkable() && !seen[self.index(n)] { seen[self.index(n)] = true; queue.push_back(n); } } } seen } } /// What someone in a city is doing there, which decides what they say and /// whether they move about. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Role { Passerby, Child, Porter, Pilgrim, Stallholder, Guard, /// The keeper of the shop with this index in `Town::shops`. Keeper(usize), Resident, Servant, Ruler, Courtier, Priest, Worshipper, Worker, Patron, Patient, Bather, Watchman, Reader, } /// Someone in the streets or indoors. pub struct Person { pub pos: Pos, pub role: Role, /// Residents, keepers and the like have names; passers-by don't. pub name: Option, pub says: &'static str, /// Stallholders, keepers, worshippers in their pews: they stay put. pub stays: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ShopKind { Broker, Doctor, Scholar, Tavern, Shipyard, /// Weapons, bought and sold. Armoury, } impl ShopKind { pub const ALL: [ShopKind; 6] = [ ShopKind::Broker, ShopKind::Doctor, ShopKind::Scholar, ShopKind::Tavern, ShopKind::Shipyard, ShopKind::Armoury, ]; /// What's painted on the roof. pub fn sign(self) -> &'static str { match self { ShopKind::Broker => "ORE", ShopKind::Doctor => "DOCTOR", ShopKind::Scholar => "SCHOLAR", ShopKind::Tavern => "TAVERN", ShopKind::Shipyard => "SHIPYARD", ShopKind::Armoury => "ARMS", } } /// "the doctor", for giving directions. pub fn trade(self) -> &'static str { match self { ShopKind::Broker => "an ore broker", ShopKind::Doctor => "a doctor", ShopKind::Scholar => "a scholar", ShopKind::Tavern => "a tavern", ShopKind::Shipyard => "a shipyard", ShopKind::Armoury => "an armourer", } } } /// What a building is for. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kind { Shop(ShopKind), House, /// Lots of small rooms, lots of people. Tenement, /// A great house, in the grand quarter. Manor, /// Where the city's ruler sits. One to a city. Palace, /// The city's great temple. One to a city. Temple, Shrine, Workshop, Warehouse, Bathhouse, Guardhouse, } /// One building, as it stands in the street. pub struct Building { pub kind: Kind, /// Its footprint on the city map. pub left: i32, pub top: i32, pub right: i32, pub bottom: i32, /// On the footprint's edge, facing the street. pub door: Pos, pub facing: Side, pub barred: bool, /// Index into `Town::shops`, if it's a shop. pub shop: Option, /// "the Sand Archive", "a house", "House Vastre". pub name: String, } impl Building { /// The street cell in front of the door. pub fn front(&self) -> Pos { let (dx, dy) = self.facing.step(); self.door.offset(dx, dy) } pub fn centre(&self) -> Pos { Pos::new((self.left + self.right) / 2, (self.top + self.bottom) / 2) } pub fn width(&self) -> i32 { self.right - self.left + 1 } pub fn height(&self) -> i32 { self.bottom - self.top + 1 } #[cfg(test)] fn rect(&self) -> Rect { Rect::new(self.left, self.top, self.right, self.bottom) } } pub struct Shop { pub kind: ShopKind, /// "the Sand Archive". pub name: String, pub keeper: String, pub building: usize, } /// The kinds of neighbourhood a city is made of. They decide how big the /// blocks and buildings are, and what the buildings are. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum QuarterKind { /// Around the square. Market, /// Inside the west gate, by the landing field. Gate, Homes, Poor, /// Where the palace and the great houses are. Grand, /// Around the temple. Holy, Works, Gardens, } pub struct Quarter { pub kind: QuarterKind, /// "the Spice Souk". pub name: String, /// Where it's centred, for the map. pub centre: Pos, } pub struct Town { /// "Erg Khaseem". pub name: String, pub culture: Culture, /// The world it's built on, and the weather today. pub land: Land, seed: u64, /// The streets, and the people in them. pub site: Site, /// Which building each cell is part of, `NONE` for none. owner: Vec, /// Which quarter each cell inside the walls is in, `NONE` outside. quarter_of: Vec, pub quarters: Vec, pub buildings: Vec, pub shops: Vec, /// The top left of your ship, which is `SHIP_SIZE` big. pub ship: Pos, /// Where you step off your ship. pub start: Pos, /// The middle of the market square. pub square: Pos, /// The middle of each gate, the west gate first. pub gates: Vec, } const NO_OWNER: u16 = u16::MAX; const NO_QUARTER: u8 = u8::MAX; impl Town { pub fn width(&self) -> i32 { self.site.width } pub fn height(&self) -> i32 { self.site.height } pub fn tile(&self, p: Pos) -> Tile { self.site.tile(p) } /// Which of its world's two sorts of `Tile::Wild` stands at `p`. pub fn wild_sort(&self, p: Pos) -> usize { let h = seed::derive(self.seed, "wild", (p.x as u64) << 32 | p.y as u64); usize::from(h.is_multiple_of(3)) } /// How much redder, greener and bluer this city's ground is than /// others on the same kind of world, from 0.92 to 1.08 times, so no two /// look quite alike. pub fn tint(&self) -> [f64; 3] { std::array::from_fn(|i| { let h = seed::derive(self.seed, "tint", i as u64); 0.92 + 0.16 * (h >> 11) as f64 / (1u64 << 53) as f64 }) } /// The building whose footprint `p` is in, door included. pub fn building_at(&self, p: Pos) -> Option { if !self.site.contains(p) { return None; } let owner = self.owner[self.site.index(p)]; (owner != NO_OWNER).then_some(usize::from(owner)) } /// The building whose street door is at `p`. pub fn door_at(&self, p: Pos) -> Option { self.building_at(p).filter(|&b| self.buildings[b].door == p) } pub fn quarter_at(&self, p: Pos) -> Option<&Quarter> { if !self.site.contains(p) { return None; } self.quarters.get(usize::from(self.quarter_of[self.site.index(p)])) } /// Goes into a building: builds its inside, the same every time. pub fn enter(&self, building: usize) -> Interior { interior::build(self, building) } /// The shop of this kind nearest to `from`. pub fn nearest_shop(&self, kind: ShopKind, from: Pos) -> Option<&Shop> { self.shops .iter() .filter(|s| s.kind == kind) .min_by_key(|s| distance2(self.buildings[s.building].door, from)) } } /// Squared distance as it looks on screen, where rows are twice as tall as /// columns are wide. pub fn distance2(a: Pos, b: Pos) -> i32 { (a.x - b.x).pow(2) + (2 * (a.y - b.y)).pow(2) } /// The seed for the capital of `place` in a system. pub fn seed_for(system_seed: u64, place: usize) -> u64 { seed::derive(system_seed, "town", place as u64) } /// What the capital `generate` would build is called, without building it. pub fn name(seed: u64, culture: Culture) -> String { culture.place(&mut seed::rng(seed)) } /// How big a capital is, from 0 to 1, for a world of this many people. pub fn size_for(population: Option) -> f64 { let people = population.unwrap_or(10_000_000).max(1) as f64; ((people.log10() - 5.3) / 4.3).clamp(0.0, 1.0) } /// Builds the capital for `seed`, `size` big (see `size_for`), on `land`. /// Only the land's terrain changes the plan: the weather doesn't move /// anything. pub fn generate(seed: u64, culture: Culture, size: f64, land: Land) -> Town { let mut rng = seed::rng(seed); // The name comes first, so `name` can find it alone. let name = culture.place(&mut rng); plan::build(name, culture, land, seed, size.clamp(0.0, 1.0), &mut rng) } /// What the world a city stands on does to it: what the ground is, and /// what the weather's doing to it today. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Land { pub terrain: Terrain, /// From 0 to 1: how hard it's frozen, on a world that isn't always. pub frost: f64, /// From 0 to 1: how parched the gardens are. pub drought: f64, } /// What's in a city's ponds today. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Pond { Water, Ice, /// Dried to cracked mud. Dry, Lava, } impl Land { /// `terrain`, on a day `temperature` warm. Only worlds with air and /// water see frost come and go and gardens wither: frost from 4°C /// down, white by -10°C; drought from 30°C up, worst by 50°C. pub fn new(terrain: Terrain, temperature: Option) -> Land { let seasons = matches!(terrain, Terrain::Desert | Terrain::Ocean | Terrain::Jungle); let c = temperature.map_or(15.0, |t| t.now - FREEZING); let (frost, drought) = if seasons { (((4.0 - c) / 14.0).clamp(0.0, 1.0), ((c - 30.0) / 20.0).clamp(0.0, 1.0)) } else { (0.0, 0.0) }; Land { terrain, frost, drought } } /// The land under the capital of `place` on `day`, if it's a planet or /// a moon. pub fn of(s: &System, place: usize, day: f64) -> Option { let kind = match s.places[place].kind { PlaceKind::Planet(p) => s.planets[p].kind, PlaceKind::Moon(p, m) => s.planets[p].moons[m].kind, PlaceKind::Belt | PlaceKind::Station(_) => return None, }; Some(Land::new(kind.terrain, survey::survey(s, place, day).temperature)) } pub fn pond(self) -> Pond { match self.terrain { Terrain::Forge => Pond::Lava, Terrain::Ice => Pond::Ice, Terrain::Barren | Terrain::Clouds => Pond::Dry, _ if self.frost > 0.6 => Pond::Ice, _ if self.drought > 0.8 => Pond::Dry, _ => Pond::Water, } } } #[cfg(test)] mod tests { use super::*; const TERRAINS: [Terrain; 6] = [ Terrain::Desert, Terrain::Ice, Terrain::Ocean, Terrain::Jungle, Terrain::Barren, Terrain::Forge, ]; fn land(terrain: Terrain) -> Land { Land::new(terrain, None) } /// A mild day on a jungle world. fn green() -> Land { land(Terrain::Jungle) } fn towns() -> impl Iterator { (0..12).flat_map(|seed| { Culture::ALL.into_iter().map(move |c| { let terrain = TERRAINS[(seed as usize + c as usize) % TERRAINS.len()]; generate(seed, c, (seed % 4) as f64 / 3.0, land(terrain)) }) }) } #[test] fn a_capitals_name_is_known_before_its_built() { for seed in 0..40 { for culture in Culture::ALL { assert_eq!(name(seed, culture), generate(seed, culture, 0.5, green()).name); } } } #[test] fn the_same_seed_builds_the_same_city() { let a = generate(7, Culture::Houses, 0.6, green()); let b = generate(7, Culture::Houses, 0.6, green()); assert_eq!(a.site.tiles, b.site.tiles); assert_eq!(a.buildings.len(), b.buildings.len()); } #[test] fn the_weather_changes_how_a_city_looks_not_where_anything_is() { let day = |now: f64| Temperature { now: FREEZING + now, coldest: 0.0, hottest: 0.0 }; let cold = Land::new(Terrain::Ocean, Some(day(-12.0))); let hot = Land::new(Terrain::Ocean, Some(day(48.0))); assert_eq!(cold.pond(), Pond::Ice); assert_eq!(hot.pond(), Pond::Dry); assert_eq!(Land::new(Terrain::Ocean, Some(day(15.0))).pond(), Pond::Water); let a = generate(4, Culture::Qaari, 0.5, cold); let b = generate(4, Culture::Qaari, 0.5, hot); assert_eq!(a.site.tiles, b.site.tiles); // Ice and forge worlds have no seasons to speak of. assert_eq!(Land::new(Terrain::Ice, Some(day(-180.0))).frost, 0.0); assert_eq!(Land::new(Terrain::Forge, Some(day(300.0))).drought, 0.0); } #[test] fn the_world_shapes_the_city() { let count = |t: &Town, tile: Tile| t.site.positions().filter(|&p| t.tile(p) == tile).count(); let mut trees = [0; 6]; let mut wild = [0; 6]; let mut ponds = [0; 6]; for seed in 0..6 { for (i, &terrain) in TERRAINS.iter().enumerate() { let t = generate(seed, Culture::Houses, 0.5, land(terrain)); trees[i] += count(&t, Tile::Tree); wild[i] += count(&t, Tile::Wild); ponds[i] += count(&t, Tile::Water); let sea = count(&t, Tile::Sea); if terrain == Terrain::Ocean { assert!(sea > count(&t, Tile::Ground), "{}: mostly sea outside", t.name); // The landing field is on dry land, and the road from it. assert!(t.site.reachable(t.start)[t.site.index(t.gates[0])]); } else { assert_eq!(sea, 0); } } } let [desert, ice, _, jungle, barren, _] = [0, 1, 2, 3, 4, 5]; assert!(trees[jungle] > 2 * trees[desert], "jungle {trees:?}"); assert!(trees[ice] < trees[jungle]); assert!(wild[jungle] > 3 * wild[desert], "jungle {wild:?}"); assert_eq!(ponds[barren], 0, "nothing pools on airless rock"); assert!(ponds[desert] < ponds[jungle], "{ponds:?}"); } #[test] fn no_two_cities_are_quite_the_same_colour() { let a = generate(1, Culture::Drift, 0.3, green()).tint(); let b = generate(2, Culture::Drift, 0.3, green()).tint(); assert_ne!(a, b); assert!(a.iter().chain(&b).all(|k| (0.92..=1.08).contains(k))); } #[test] fn bigger_worlds_have_bigger_capitals() { let small = generate(3, Culture::Qaari, size_for(Some(300_000)), green()); let big = generate(3, Culture::Qaari, size_for(Some(3_000_000_000)), green()); let area = |t: &Town| t.width() * t.height(); assert!(area(&big) * 2 > 5 * area(&small)); assert!(big.buildings.len() > 2 * small.buildings.len()); assert!(small.buildings.len() > 60, "even a small capital is a city"); } #[test] fn every_door_opens_onto_a_street_you_can_reach() { for t in towns() { let seen = t.site.reachable(t.start); for (i, b) in t.buildings.iter().enumerate() { assert_eq!(t.building_at(b.door), Some(i)); assert!(t.tile(b.front()).paved(), "{}: door of {} opens onto nothing", t.name, i); assert!(seen[t.site.index(b.front())], "{}: can't reach {}", t.name, b.name); assert_eq!(t.tile(b.door), Tile::Door { open: !b.barred }); for p in b.rect().cells() { assert_eq!(t.building_at(p), Some(i), "buildings don't overlap"); } } for p in &t.site.people { assert!(t.tile(p.pos).walkable()); assert!(seen[t.site.index(p.pos)], "{}: someone's stuck at {:?}", t.name, p.pos); } } } #[test] fn every_city_has_every_shop_and_a_palace_and_temple() { for t in towns() { for kind in ShopKind::ALL { let n = t.shops.iter().filter(|s| s.kind == kind).count(); if kind == ShopKind::Tavern { assert!(n >= 1) } else { assert_eq!(n, 1) } } for (i, shop) in t.shops.iter().enumerate() { let b = &t.buildings[shop.building]; assert_eq!(b.shop, Some(i)); assert!(!b.barred); assert!( b.width() >= shop.kind.sign().len() as i32 + 2, "{}: the sign fits", t.name ); } for kind in [Kind::Palace, Kind::Temple] { let n = t.buildings.iter().filter(|b| b.kind == kind).count(); assert_eq!(n, 1, "{}: {kind:?}", t.name); } let kinds: std::collections::HashSet<_> = t.buildings.iter().map(|b| std::mem::discriminant(&b.kind)).collect(); assert!(kinds.len() >= 8, "{}: a city has all sorts of buildings", t.name); } } #[test] fn passers_by_stay_outdoors() { let mut t = generate(5, Culture::Drift, 0.4, green()); let mut rng = seed::rng(1); for _ in 0..300 { for who in 0..t.site.people.len() { t.site.wander(who, &mut rng, t.start); } } for p in &t.site.people { assert!(t.tile(p.pos).outdoors() || p.stays, "{:?} wandered off the street", p.pos); } let mut places: Vec = t.site.people.iter().map(|p| p.pos).collect(); places.sort_by_key(|p| (p.x, p.y)); places.dedup(); assert_eq!(places.len(), t.site.people.len(), "no two people in one place"); } }