use crate::world::types::{DemographicsOutput, Settlement};
#[derive(Debug, Clone, PartialEq)]
pub struct Founding {
pub year: i64,
pub label: String,
pub class: String,
pub population: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Epoch {
pub name: String,
pub start_year: i64,
pub end_year: i64,
pub note: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistEvent {
pub year: i64,
pub kind: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistoryOutput {
pub foundings: Vec<Founding>,
pub epochs: Vec<Epoch>,
pub events: Vec<HistEvent>,
pub span_years: i64,
}
fn mix(a: u64, seed: u64) -> u64 {
a.wrapping_mul(2_654_435_761).wrapping_add(seed.wrapping_mul(40_503)).wrapping_add(0x9E37)
}
fn basis_weight(basis: &str) -> f64 {
match basis {
"river_mouth" => 3.0,
"confluence" => 2.0,
"fertile_valley" => 1.5,
_ => 1.0,
}
}
fn jitter(s: &Settlement, seed: u64) -> f64 {
let h = (s.x as u64)
.wrapping_mul(2_654_435_761)
.wrapping_add((s.y as u64).wrapping_mul(40_503))
.wrapping_add(seed);
(h % 1000) as f64 / 1000.0
}
fn antiquity(s: &Settlement, seed: u64) -> f64 {
(s.population.max(1) as f64).ln() * basis_weight(&s.basis) + jitter(s, seed)
}
pub fn compile_history(
demo: &DemographicsOutput,
declared: &[crate::world::types::HistEventDef],
seed: u64,
) -> HistoryOutput {
let mut settlements: Vec<&Settlement> = demo.settlements.iter().collect();
settlements.sort_by(|a, b| {
antiquity(b, seed)
.partial_cmp(&antiquity(a, seed))
.unwrap_or(std::cmp::Ordering::Equal)
});
let span = (200 + demo.settlements.len() as i64 * 20).min(4000);
let n = settlements.len().max(1) as i64;
let foundings: Vec<Founding> = settlements
.iter()
.enumerate()
.map(|(i, s)| Founding {
year: -span + (span * i as i64) / n,
label: format!("the {} {} on the {}", s.biome, s.class, s.basis.replace('_', " ")),
class: s.class.clone(),
population: s.population,
})
.collect();
let third = (span / 3).max(1);
let epochs = vec![
Epoch {
name: "Founding Age".into(),
start_year: -span,
end_year: -span + third,
note: "the first settlements take root on the best sites".into(),
},
Epoch {
name: "Age of Expansion".into(),
start_year: -span + third,
end_year: -third,
note: "towns spread along the rivers and coasts".into(),
},
Epoch {
name: "Present Age".into(),
start_year: -third,
end_year: 0,
note: "the world as the story finds it".into(),
},
];
let mut events: Vec<HistEvent> = Vec::new();
let pol = super::polities_layer::compile_polities(demo, &[], seed);
for (i, p) in pol.polities.iter().enumerate() {
let rise = -span + third * ((mix(i as u64, seed) % 3) as i64);
events.push(HistEvent {
year: rise,
kind: "rise".into(),
description: format!("the realm of {} rises around {}", p.name, p.capital),
});
if mix(i as u64, seed.wrapping_add(11)) % 3 == 0 {
events.push(HistEvent {
year: -third + span / 6,
kind: "fall".into(),
description: format!("the realm of {} wanes", p.name),
});
}
}
let mut biomes: Vec<String> = Vec::new();
for s in &demo.settlements {
if !biomes.contains(&s.biome) {
biomes.push(s.biome.clone());
}
}
if biomes.len() >= 2 {
for e in 0..2u64 {
let a = &biomes[(mix(e, seed) % biomes.len() as u64) as usize];
let b = &biomes[(mix(e.wrapping_add(7), seed.wrapping_mul(3)) % biomes.len() as u64) as usize];
if a != b {
events.push(HistEvent {
year: -span + third + (e as i64) * (third / 2),
kind: "migration".into(),
description: format!("a people migrate from the {a} to the {b}"),
});
}
}
}
for d in declared {
events.push(HistEvent {
year: d.year,
kind: "declared".into(),
description: d.title.clone(),
});
}
events.sort_by_key(|e| e.year);
HistoryOutput { foundings, epochs, events, span_years: span }
}
pub fn epoch_of(year: i64, epochs: &[Epoch]) -> Option<&Epoch> {
epochs.iter().find(|e| year >= e.start_year && year < e.end_year)
}
pub fn lint_history(declared: &[crate::world::types::HistEventDef], out: &HistoryOutput) -> Vec<String> {
let mut warnings = Vec::new();
for d in declared {
let label = if d.title.trim().is_empty() {
format!("event at year {}", d.year)
} else {
format!("event `{}`", d.title)
};
if d.year > 0 {
warnings.push(format!("{label}: year {} is after the present (0)", d.year));
} else if d.year < -out.span_years {
warnings.push(format!(
"{label}: year {} is before recorded history began ({})",
d.year, -out.span_years
));
}
if let Some(declared_epoch) = &d.epoch {
match epoch_of(d.year, &out.epochs) {
Some(e) if !e.name.eq_ignore_ascii_case(declared_epoch) => warnings.push(format!(
"{label}: pinned to epoch `{declared_epoch}`, but year {} falls in `{}`",
d.year, e.name
)),
_ => {}
}
}
}
warnings
}
#[cfg(test)]
mod tests {
use super::*;
fn settle(x: usize, y: usize, pop: u64, class: &str, basis: &str) -> Settlement {
Settlement {
x,
y,
population: pop,
class: class.into(),
basis: basis.into(),
biome: "temperate".into(),
}
}
fn demo(settlements: Vec<Settlement>) -> DemographicsOutput {
DemographicsOutput {
total_population: settlements.iter().map(|s| s.population).sum(),
habitable_fraction: 0.5,
settlements,
size_classes: Default::default(),
role_archetypes: Vec::new(),
}
}
#[test]
fn oldest_settlement_is_the_largest_best_sited() {
let d = demo(vec![
settle(1, 1, 500, "village", "coast"),
settle(2, 2, 90_000, "city", "river_mouth"),
settle(3, 3, 5_000, "town", "fertile_valley"),
]);
let h = compile_history(&d, &[], 0x1234);
assert_eq!(h.foundings.first().unwrap().class, "city");
assert!(h.foundings.first().unwrap().year < h.foundings.last().unwrap().year);
assert_eq!(h.foundings.len(), 3);
assert_eq!(h.epochs.len(), 3);
}
#[test]
fn generates_rise_and_migration_events() {
let d = demo(vec![
settle(0, 0, 90_000, "city", "river_mouth"),
settle(40, 40, 80_000, "city", "confluence"),
settle(1, 1, 3_000, "town", "fertile_valley"),
settle(41, 39, 2_000, "town", "coast"),
]);
let mut d = d;
d.settlements[1].biome = "hot_desert".into();
d.settlements[3].biome = "taiga".into();
let h = compile_history(&d, &[], 0x55);
assert!(h.events.iter().any(|e| e.kind == "rise"));
assert!(h.events.iter().any(|e| e.kind == "migration"));
assert!(h.events.windows(2).all(|w| w[0].year <= w[1].year));
}
#[test]
fn declared_events_merge_and_are_linted() {
use crate::world::types::HistEventDef;
let d = demo(vec![
settle(0, 0, 90_000, "city", "river_mouth"),
settle(5, 5, 3_000, "town", "fertile_valley"),
]);
let declared = vec![
HistEventDef { year: -100, title: "The Sundering".into(), epoch: None, places: None, description: String::new() },
HistEventDef { year: 50, title: "An Impossible Future".into(), epoch: None, places: None, description: String::new() },
];
let h = compile_history(&d, &declared, 0x1);
assert!(h.events.iter().any(|e| e.kind == "declared" && e.description == "The Sundering"));
let w = lint_history(&declared, &h);
assert!(w.iter().any(|s| s.contains("after the present")));
let bad_epoch = vec![HistEventDef {
year: -50, title: "Misdated".into(), epoch: Some("Founding Age".into()), places: None, description: String::new(),
}];
assert!(lint_history(&bad_epoch, &h).iter().any(|s| s.contains("falls in")));
}
#[test]
fn is_deterministic() {
let d = demo(vec![
settle(1, 1, 500, "village", "coast"),
settle(2, 2, 90_000, "city", "river_mouth"),
]);
assert_eq!(compile_history(&d, &[], 7), compile_history(&d, &[], 7));
}
#[test]
fn span_scales_with_settlement_count_and_is_capped() {
let one = demo(vec![settle(1, 1, 100, "village", "coast")]);
let many = demo((0..500).map(|i| settle(i, i, 100, "village", "coast")).collect());
assert!(compile_history(&one, &[], 0).span_years < compile_history(&many, &[], 0).span_years);
assert!(compile_history(&many, &[], 0).span_years <= 4000);
}
}