summaryrefslogtreecommitdiff
path: root/src/town/mod.rs
diff options
context:
space:
mode:
authorskylar <me@casper>2026-11-11 15:24:37 -0800
committerskylar <me@casper>2026-11-11 15:24:37 -0800
commit9ccf951473e21a8b90c25161156fa09a9b227a7b (patch)
treee7bd9858a4cdbc5ae50069db8ca0cf868e436dec /src/town/mod.rs
parentd36c54d996f3c14ae811312dbbe621e12a39edde (diff)
Orrery 0.1.0: the galaxy, flight, cities, inventory and saves
Grows the derelict crawler into the whole game, and gets it ready to publish. - A galaxy of star systems joined by lanes, split between four peoples' territories, with a star map and a system map. Systems are modelled with Kepler orbits, moons inside their Hill spheres, belts past the frost line and stations at Lagrange points. - Flight around one place at a time: arcade flight with a throttle fallback where held keys can't be detected, mining rocks for ore, landing, docking at stations, and boarding wrecks that come and go by the calendar. - Your ship to walk about, and capital cities to land at: generated plans with walls, quarters and streets, building interiors, shops, taverns, scholars and armourers. - Fighting with a weapon and a gun ready, six damage types and how each monster takes them, and armour that guards, wears and can be mended. - Everything you own has a weight: a vim-keyed inventory, a pack and the ship's lockers, and trade in ore, salvage, arms and fuel. - Characters with a people and a ship of their own, saved as versioned JSON on every scene change and on quit, with a character sheet, a saves list and a pause menu wherever you are. - `orrery weapons`, `armour`, `galaxy`, `system` and `town` print the generators' output without the TUI. - Release: README install steps for macOS and Linux, a Makefile whose install also works with macOS's install, an MIT licence, and Cargo metadata with rust-version 1.88. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/town/mod.rs')
-rw-r--r--src/town/mod.rs850
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");
+ }
+}