use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::world::types::DemographicsOutput;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PlaceProposal {
pub id: Uuid,
pub signature: String,
pub kind: String,
pub name: String,
pub payload: serde_json::Value,
pub rationale: String,
pub status: String,
pub created_at: i64,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PlaceLink {
pub place_id: Uuid,
pub name: String,
pub biome: String,
pub climate_zone: String,
pub hydrology_basis: String,
pub population: u64,
pub x: usize,
pub y: usize,
}
impl PlaceLink {
pub fn from_proposal(place_id: Uuid, p: &PlaceProposal) -> Self {
let s = |k: &str| p.payload.get(k).and_then(|v| v.as_str()).unwrap_or("").to_string();
let u = |k: &str| p.payload.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
let biome = s("biome");
PlaceLink {
place_id,
name: p.name.clone(),
climate_zone: biome.clone(), biome,
hydrology_basis: s("basis"),
population: u("population"),
x: u("x") as usize,
y: u("y") as usize,
}
}
}
pub fn place_proposals(demo: &DemographicsOutput, seed: u64) -> Vec<PlaceProposal> {
let now = now_secs();
demo.settlements
.iter()
.map(|s| {
let name = settlement_name(seed, s.x, s.y);
let payload = serde_json::json!({
"name": name,
"x": s.x,
"y": s.y,
"population": s.population,
"class": s.class,
"basis": s.basis,
"biome": s.biome,
});
let rationale = format!(
"A {} of ~{} at a {} in a {} zone.",
s.class,
fmt_pop(s.population),
s.basis.replace('_', " "),
s.biome.replace('_', " "),
);
PlaceProposal {
id: Uuid::new_v4(),
signature: format!("place:{}:{}", s.x, s.y),
kind: "place".into(),
name,
payload,
rationale,
status: "pending".into(),
created_at: now,
}
})
.collect()
}
fn fmt_pop(n: u64) -> String {
if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 10_000 {
format!("{:.0}k", n as f64 / 1_000.0)
} else {
n.to_string()
}
}
pub fn settlement_name(seed: u64, x: usize, y: usize) -> String {
const ONSET: &[&str] =
&["V", "K", "M", "T", "S", "L", "R", "N", "Th", "Br", "Dr", "El", "Ar", "Or", "Vel", "Kor"];
const NUCLEUS: &[&str] = &["a", "e", "i", "o", "u", "ae", "ia", "ar", "en"];
const CODA: &[&str] = &["n", "r", "l", "s", "th", "m", "", "k", "ndor", "mar"];
let mut rng =
SplitMix64::new(seed ^ (x as u64).wrapping_mul(73856093) ^ (y as u64).wrapping_mul(19349663));
let syllables = 2 + (rng.next_u64() % 2) as usize; let mut name = String::new();
for s in 0..syllables {
name.push_str(ONSET[(rng.next_u64() as usize) % ONSET.len()]);
name.push_str(NUCLEUS[(rng.next_u64() as usize) % NUCLEUS.len()]);
if s + 1 == syllables {
name.push_str(CODA[(rng.next_u64() as usize) % CODA.len()]);
}
}
let mut chars = name.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
None => name,
}
}
pub(crate) fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
SplitMix64(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_deterministic_and_vary_by_position() {
assert_eq!(settlement_name(42, 60, 69), settlement_name(42, 60, 69));
assert_ne!(settlement_name(42, 60, 69), settlement_name(42, 61, 69));
let n = settlement_name(42, 60, 69);
assert!(n.chars().next().unwrap().is_uppercase());
assert!(n.len() >= 3 && n.chars().all(|c| c.is_alphabetic()));
}
}