diff options
Diffstat (limited to 'src/town/mod.rs')
| -rw-r--r-- | src/town/mod.rs | 850 |
1 files changed, 850 insertions, 0 deletions
diff --git a/src/town/mod.rs b/src/town/mod.rs new file mode 100644 index 0000000..fd17b20 --- /dev/null +++ b/src/town/mod.rs @@ -0,0 +1,850 @@ +//! 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<Item = Pos> { + (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<Tile>, + pub people: Vec<Person>, + /// 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<Item = Pos> + use<> { + Rect::new(0, 0, self.width - 1, self.height - 1).cells() + } + + pub fn person_at(&self, p: Pos) -> Option<usize> { + 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<Pos> = 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<bool> { + 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<String>, + 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<usize>, + /// "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<u16>, + /// Which quarter each cell inside the walls is in, `NONE` outside. + quarter_of: Vec<u8>, + pub quarters: Vec<Quarter>, + pub buildings: Vec<Building>, + pub shops: Vec<Shop>, + /// 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<Pos>, +} + +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<usize> { + 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<usize> { + 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<u64>) -> 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<Temperature>) -> 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<Land> { + 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<Item = Town> { + (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<Pos> = 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"); + } +} |