feat(simulation): build Tier-3 corporation generation pipeline (#800)

Adds server/src/bin/generate_corporations and tooling/generate-corporations
wrapper. Generates ~5,000 Tier-3 corp instances from Tier-1/2 template
archetypes with seeded name generation (FNV-1a + corridor-weighted PRNG).
Writes wiki markdown stubs for each generated corporation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 13:55:47 +02:00
co-authored by Claude Sonnet 4.6
parent 3447b38bcb
commit 7b0465a8c6
5 changed files with 1322 additions and 3 deletions
+54 -3
View File
@@ -226,7 +226,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn",
"toml_edit",
"toml_edit 0.23.10+spec-1.0.0",
]
[[package]]
@@ -1221,6 +1221,15 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@@ -1236,7 +1245,7 @@ dependencies = [
[[package]]
name = "settled-reach-server"
version = "0.1.31"
version = "0.1.32"
dependencies = [
"bevy_app",
"bevy_ecs",
@@ -1254,6 +1263,7 @@ dependencies = [
"serde_yaml",
"sysinfo",
"thiserror",
"toml",
"tracing",
"tracing-subscriber",
]
@@ -1378,6 +1388,27 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
@@ -1387,6 +1418,20 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_write",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
@@ -1394,7 +1439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
"toml_datetime",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
]
@@ -1408,6 +1453,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tracing"
version = "0.1.44"
+1
View File
@@ -22,6 +22,7 @@ crossbeam-channel = "0.5"
sysinfo = "0.35"
serde_json = "1"
rusqlite = { version = "0.32", features = ["bundled"] }
toml = "0.8"
[features]
default = ["gauntlet"]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,214 @@
//! Deterministic name generation for Tier-3 corporations.
//!
//! Names are composed from culture-specific pools matching the Settled Reach's
//! geographic sectors. Each sector has dominant cultural influences derived
//! from lore (wiki settlements, founding cultures, corridor identities).
//!
//! Pattern: `{surname/word} {business_suffix}` where surname draws from
//! the sector's cultural pool and suffix from the lore category.
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
// ---------------------------------------------------------------------------
// Surname pools by sector (drawn from founding cultures in wiki canon)
// ---------------------------------------------------------------------------
/// Core systems: cosmopolitan mix — the Reach's center of gravity.
const CORE_NAMES: &[&str] = &[
"Alvarez", "Benoit", "Carvalho", "Durand", "Eriksen", "Fournier", "Gao",
"Hartmann", "Ishida", "Johansson", "Kirchner", "Lemaire", "Moreau",
"Nakamura", "Olsson", "Pelletier", "Richter", "Saito", "Torres",
"Ueda", "Vasquez", "Werner", "Xu", "Yamada", "Zhou", "Andersen",
"Beaumont", "Costa", "Delacroix", "Engel", "Fujita", "Gutierrez",
"Hayashi", "Ibarra", "Jensen", "Klein", "Laurent", "Mercier",
"Novak", "Ortiz", "Park", "Reuter", "Suzuki", "Takahashi",
"Ulrich", "Valentin", "Wagner", "Xie", "Yilmaz", "Zhang",
];
/// North reach: Nordic, Scottish, northern European — Calloway heritage.
const NORTH_REACH_NAMES: &[&str] = &[
"Andersson", "Bjornsson", "Calloway", "Dalsgaard", "Eklund", "Falk",
"Grimstad", "Hedlund", "Ivarsson", "Jonasson", "Kirkpatrick", "Lindqvist",
"MacLeod", "Nordstrom", "Olafsson", "Pettersson", "Rehn", "Strandberg",
"Thorsen", "Ulvskog", "Vikstrom", "Wahlberg", "Aberg", "Berglund",
"Carlsen", "Dalgaard", "Engstrom", "Forsell", "Gustafsson", "Halvorsen",
"Ingvarsson", "Jansson", "Knudsen", "Lundin", "MacPherson", "Nylund",
"Ostergaard", "Palsson", "Rasmussen", "Sjoberg", "Toft", "Ulfsson",
"Vestergaard", "Wiklund", "Aasen", "Brannstrom", "Dahl", "Eide",
"Friberg", "Gren",
];
/// South reach: Eastern European, East Asian industrial — Stalownia corridor.
const SOUTH_REACH_NAMES: &[&str] = &[
"Adamski", "Baranov", "Chernov", "Dubois", "Egorov", "Filipov",
"Gromov", "Horvat", "Ivanova", "Jankovic", "Kowalski", "Lazarev",
"Morozov", "Novikov", "Ostrowski", "Petrov", "Reznik", "Sokolov",
"Tkachenko", "Uvarov", "Volkov", "Wojcik", "Yakimov", "Zheng",
"Babic", "Chernyshev", "Dragunov", "Fedorov", "Grushevsky", "Havel",
"Ito", "Jovanovic", "Katsaros", "Lebedev", "Mazur", "Nemec",
"Ochoa", "Popov", "Radic", "Smirnov", "Tanaka", "Urasawa",
"Vasiliev", "Watanabe", "Xiang", "Yegorov", "Zaytsev", "Borysko",
"Chen", "Dimitrov",
];
/// West reach: German, Central European — Compact territory, Westphalian influence.
const WEST_REACH_NAMES: &[&str] = &[
"Albrecht", "Baumann", "Christensen", "Dietrich", "Eisenberg", "Fischer",
"Gruber", "Hoffmann", "Ingolstadt", "Jaeger", "Kessler", "Lehmann",
"Mueller", "Neumann", "Obermann", "Pfeiffer", "Quandt", "Roth",
"Schaefer", "Thiel", "Urban", "Vogt", "Weidenfeld", "Ziegler",
"Becker", "Claussen", "Dorfmann", "Eberhardt", "Fleischer", "Gerstner",
"Haber", "Imhof", "Jung", "Kraemer", "Linden", "Metzger",
"Niedermann", "Opitz", "Preuss", "Raabe", "Steinbach", "Trautmann",
"Unger", "Vollmer", "Winterberg", "Zahn", "Auerbach", "Bruckner",
"Dahlem", "Eckhardt",
];
/// East reach: Filipino, Korean, maritime Asian — distinctive identity.
const EAST_REACH_NAMES: &[&str] = &[
"Aquino", "Bautista", "Cruz", "Dalisay", "Espiritu", "Flores",
"Garcia", "Hernandez", "Ilagan", "Jeon", "Kim", "Lim",
"Magalang", "Navarro", "Ocampo", "Park", "Quijano", "Reyes",
"Santos", "Tan", "Uy", "Villanueva", "Wong", "Yoo",
"Aguilar", "Buenaventura", "Castillo", "Dizon", "Enriquez", "Fernandez",
"Gonzales", "Hwang", "Ignacio", "Jeong", "Kwon", "Lee",
"Marasigan", "Nakamura", "Oh", "Perez", "Ramos", "Son",
"Tolentino", "Umali", "Valdez", "Yun", "Zamora", "Baek",
"Choi", "Dela Cruz",
];
/// Deep frontier: mixed backgrounds from all settler waves — no dominant culture.
const FRONTIER_NAMES: &[&str] = &[
"Adeyemi", "Bergstrom", "Chandra", "Duval", "Emeka", "Fonseca",
"Gupta", "Hassan", "Ibrahim", "Jansson", "Kovac", "Liu",
"Martinez", "Nkosi", "Okafor", "Patel", "Quinn", "Rodriguez",
"Sousa", "Thorne", "Uddin", "Varga", "Wu", "Xiong",
"Yoshida", "Zhao", "Abara", "Beaumont", "Cardenas", "Doyle",
"Ekwueme", "Ferreira", "Gomes", "Henriksen", "Idris", "Juma",
"Kato", "Larsen", "Morales", "Ndlovu", "Osei", "Petrov",
"Ruiz", "Singh", "Tavares", "Uchida", "Volkov", "Wang",
"Yang", "Zaman",
];
// ---------------------------------------------------------------------------
// Business suffix pools by lore category
// ---------------------------------------------------------------------------
const EXTRACTION_SUFFIXES: &[&str] = &[
"Mining Co.", "Extraction", "Resources", "Minerals", "Mining",
"Quarry Works", "Deep Drill", "Ore Works", "Claims", "Mining & Salvage",
"Prospecting", "Dig Co.", "Rock Works", "Shaft Mining", "Surface Mining",
];
const AGRICULTURE_SUFFIXES: &[&str] = &[
"Farms", "Agricultural Co.", "Growers", "Harvest", "Provisions",
"Ranchers", "Fisheries", "Food Co.", "Plantations", "Cultivators",
"Produce", "Orchard", "Dairy", "Stockfeed", "Processing",
];
const MANUFACTURING_SUFFIXES: &[&str] = &[
"Manufacturing", "Works", "Industries", "Fabrication", "Engineering",
"Precision", "Assembly", "Components", "Foundry", "Machine Works",
"Systems", "Technical", "Metalworks", "Forging", "Production",
];
const TRADE_LOGISTICS_SUFFIXES: &[&str] = &[
"Freight", "Logistics", "Shipping", "Transport", "Haulage",
"Cargo", "Transit", "Distribution", "Forwarding", "Express",
"Lines", "Carriers", "Fleet", "Couriers", "Supply Co.",
];
const SERVICES_SUFFIXES: &[&str] = &[
"Services", "Associates", "Consulting", "Partners", "Group",
"Holdings", "Clinic", "Bureau", "Agency", "Office",
"Practice", "Solutions", "Advisors", "Trust", "Institute",
];
const INTELLIGENCE_SUFFIXES: &[&str] = &[
"Analytics", "Intelligence", "Data Services", "Information", "Research",
"Advisory", "Insights", "Consulting", "Networks", "Analysis",
];
// ---------------------------------------------------------------------------
// Name generation
// ---------------------------------------------------------------------------
fn names_for_sector(sector: &str) -> &'static [&'static str] {
match sector {
"core" => CORE_NAMES,
"north_reach" => NORTH_REACH_NAMES,
"south_reach" => SOUTH_REACH_NAMES,
"west_reach" => WEST_REACH_NAMES,
"east_reach" => EAST_REACH_NAMES,
"deep_frontier" => FRONTIER_NAMES,
_ => CORE_NAMES,
}
}
fn suffixes_for_category(category: &str) -> &'static [&'static str] {
match category {
"extraction" => EXTRACTION_SUFFIXES,
"agriculture" => AGRICULTURE_SUFFIXES,
"manufacturing" => MANUFACTURING_SUFFIXES,
"trade_logistics" => TRADE_LOGISTICS_SUFFIXES,
"services" => SERVICES_SUFFIXES,
"intelligence" => INTELLIGENCE_SUFFIXES,
_ => SERVICES_SUFFIXES,
}
}
/// Generate a plausible business name for the given sector and lore category.
/// Deterministic for a given RNG state.
pub fn generate_name(rng: &mut ChaCha8Rng, sector: &str, category: &str) -> String {
let names = names_for_sector(sector);
let suffixes = suffixes_for_category(category);
let surname = names[rng.random_range(0..names.len())];
let suffix = suffixes[rng.random_range(0..suffixes.len())];
// 20% chance of double-barrel name (Surname & Surname Suffix)
if rng.random::<f64>() < 0.20 {
let surname2 = names[rng.random_range(0..names.len())];
if surname != surname2 {
return format!("{} & {} {}", surname, surname2, suffix);
}
}
// 15% chance of "Surname's Suffix" or "Surname Bros. Suffix"
if rng.random::<f64>() < 0.15 {
return format!("{} Bros. {}", surname, suffix);
}
format!("{} {}", surname, suffix)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
#[test]
fn deterministic_names() {
let mut rng1 = ChaCha8Rng::seed_from_u64(42);
let mut rng2 = ChaCha8Rng::seed_from_u64(42);
for _ in 0..100 {
let a = generate_name(&mut rng1, "core", "extraction");
let b = generate_name(&mut rng2, "core", "extraction");
assert_eq!(a, b);
}
}
#[test]
fn names_not_empty() {
let mut rng = ChaCha8Rng::seed_from_u64(1);
for sector in &["core", "north_reach", "south_reach", "west_reach", "east_reach", "deep_frontier"] {
for cat in &["extraction", "agriculture", "manufacturing", "trade_logistics", "services", "intelligence"] {
let name = generate_name(&mut rng, sector, cat);
assert!(!name.is_empty(), "Empty name for {}/{}", sector, cat);
assert!(name.contains(' '), "No space in name: {}", name);
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Generate Tier-3 corporations for the Settled Reach economy.
#
# Usage:
# tooling/generate-corporations
# tooling/generate-corporations --seed 42 --min-corps 5000
# tooling/generate-corporations --output path/to/output.toml
#
# Builds on first run if binary doesn't exist.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BIN="$ROOT_DIR/server/target/debug/generate_corporations"
# Build if needed
if [ ! -f "$BIN" ]; then
echo "Building generate_corporations..." >&2
(cd "$ROOT_DIR/server" && cargo build --bin generate_corporations 2>&1 | tail -3) >&2
fi
exec "$BIN" "$@"