sim/src/world.rs

2273 lines
74 KiB
Rust
Raw Normal View History

use picard::actors::{
Actor, ActorFactoryArgs, ActorRef, ActorRefFactory, ActorReference, ActorSystem,
Context as ActorContext, Sender, Tell,
};
use rune::termcolor::Buffer;
use rune::{Any, Context, Diagnostics, Module, Source, Sources, Vm};
use serde::{Deserialize, Serialize};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet};
use std::fmt::Write;
use std::sync::{
Arc,
mpsc::{SyncSender, sync_channel},
};
use std::time::Duration;
pub const W: usize = 96;
pub const H: usize = 64;
pub const DEFAULT_TICKS_PER_DAY: u64 = 1800;
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct StageDuration {
pub min_days: u32,
pub max_days: u32,
#[serde(default)]
pub fertile: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CalendarSettings {
pub ticks_per_day: u64,
pub days_per_week: u32,
pub weeks_per_month: u32,
pub months_per_year: u32,
pub toddler: StageDuration,
pub child: StageDuration,
pub teen: StageDuration,
pub young_adult: StageDuration,
pub adult: StageDuration,
pub older_adult: StageDuration,
pub elderly: StageDuration,
}
impl Default for StageDuration {
fn default() -> Self {
Self {
min_days: 1,
max_days: 2,
fertile: false,
}
}
}
impl Default for CalendarSettings {
fn default() -> Self {
Self {
ticks_per_day: DEFAULT_TICKS_PER_DAY,
days_per_week: 7,
weeks_per_month: 4,
months_per_year: 12,
toddler: StageDuration {
min_days: 2,
max_days: 4,
fertile: false,
},
child: StageDuration {
min_days: 4,
max_days: 8,
fertile: false,
},
teen: StageDuration {
min_days: 4,
max_days: 8,
fertile: false,
},
young_adult: StageDuration {
min_days: 6,
max_days: 12,
fertile: true,
},
adult: StageDuration {
min_days: 20,
max_days: 40,
fertile: true,
},
older_adult: StageDuration {
min_days: 10,
max_days: 20,
fertile: false,
},
elderly: StageDuration {
min_days: 4,
max_days: 10,
fertile: false,
},
}
}
}
impl CalendarSettings {
fn duration(&self, stage: &str) -> Option<&StageDuration> {
match stage {
"toddler" => Some(&self.toddler),
"child" => Some(&self.child),
"teen" => Some(&self.teen),
"young_adult" => Some(&self.young_adult),
"adult" => Some(&self.adult),
"older_adult" => Some(&self.older_adult),
"elderly" => Some(&self.elderly),
_ => None,
}
}
fn normalize(&mut self) {
self.ticks_per_day = self.ticks_per_day.clamp(1, 1_000_000);
self.days_per_week = self.days_per_week.clamp(1, 100);
self.weeks_per_month = self.weeks_per_month.clamp(1, 100);
self.months_per_year = self.months_per_year.clamp(1, 100);
for stage in [
"toddler",
"child",
"teen",
"young_adult",
"adult",
"older_adult",
"elderly",
] {
let range = self.duration_mut(stage).expect("known life stage");
range.min_days = range.min_days.clamp(1, 1_000_000);
range.max_days = range.max_days.clamp(range.min_days, 1_000_000);
}
}
fn duration_mut(&mut self, stage: &str) -> Option<&mut StageDuration> {
match stage {
"toddler" => Some(&mut self.toddler),
"child" => Some(&mut self.child),
"teen" => Some(&mut self.teen),
"young_adult" => Some(&mut self.young_adult),
"adult" => Some(&mut self.adult),
"older_adult" => Some(&mut self.older_adult),
"elderly" => Some(&mut self.elderly),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum LifeStage {
#[default]
Toddler,
Child,
Teen,
YoungAdult,
Adult,
OlderAdult,
Elderly,
Dead,
}
impl LifeStage {
fn next(self) -> Option<Self> {
match self {
Self::Toddler => Some(Self::Child),
Self::Child => Some(Self::Teen),
Self::Teen => Some(Self::YoungAdult),
Self::YoungAdult => Some(Self::Adult),
Self::Adult => Some(Self::OlderAdult),
Self::OlderAdult => Some(Self::Elderly),
Self::Elderly => Some(Self::Dead),
Self::Dead => None,
}
}
fn key(self) -> &'static str {
match self {
Self::Toddler => "toddler",
Self::Child => "child",
Self::Teen => "teen",
Self::YoungAdult => "young_adult",
Self::Adult => "adult",
Self::OlderAdult => "older_adult",
Self::Elderly => "elderly",
Self::Dead => "dead",
}
}
fn can_partner(self) -> bool {
matches!(
self,
Self::YoungAdult | Self::Adult | Self::OlderAdult | Self::Elderly
)
}
}
const WATER_DEEP: u8 = 0;
const WATER: u8 = 1;
const SAND: u8 = 2;
const GRASS: u8 = 3;
const FOREST: u8 = 4;
const HILLS: u8 = 5;
const MOUNTAIN: u8 = 6;
const TREE: u8 = 1;
const BERRY: u8 = 2;
const STONE: u8 = 3;
const BASE_POPULATION_CAPACITY: usize = 12;
const STARTING_FOOD: u32 = 60;
pub struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545F4914F6CDD1D)
}
fn f(&mut self) -> f32 {
(self.next() >> 40) as f32 / (1u64 << 24) as f32
}
fn chance(&mut self, p: f32) -> bool {
self.f() < p
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
}
fn hash(x: i32, y: i32, seed: u64) -> f32 {
let mut h = (x as u64).wrapping_mul(0x9E3779B97F4A7C15)
^ (y as u64).wrapping_mul(0xC2B2AE3D27D4EB4F)
^ seed.wrapping_mul(0x165667B19E3779F9);
h ^= h >> 33;
h = h.wrapping_mul(0xFF51AFD7ED558CCD);
h ^= h >> 33;
(h >> 40) as f32 / (1u64 << 24) as f32
}
fn value_noise(x: f32, y: f32, seed: u64) -> f32 {
let (xi, yi) = (x.floor() as i32, y.floor() as i32);
let (fx, fy) = (x - xi as f32, y - yi as f32);
let s = |t: f32| t * t * (3.0 - 2.0 * t);
let (u, v) = (s(fx), s(fy));
let a = hash(xi, yi, seed);
let b = hash(xi + 1, yi, seed);
let c = hash(xi, yi + 1, seed);
let d = hash(xi + 1, yi + 1, seed);
let top = a + (b - a) * u;
let bot = c + (d - c) * u;
top + (bot - top) * v
}
fn fbm(x: f32, y: f32, seed: u64) -> f32 {
let (mut sum, mut amp, mut freq, mut norm) = (0.0, 1.0, 1.0 / 16.0, 0.0);
for o in 0..4 {
sum += amp * value_noise(x * freq, y * freq, seed + o);
norm += amp;
amp *= 0.5;
freq *= 2.0;
}
sum / norm
}
/// What an entity senses this tick; passed to the Rune behaviour scripts.
#[derive(Any, Default, Clone)]
struct Ctx {
#[rune(get)]
night: bool,
#[rune(get)]
at_home: bool,
#[rune(get)]
hunger: f64,
#[rune(get)]
carry: i64,
#[rune(get)]
carried_food: i64,
#[rune(get)]
stock_food: i64,
#[rune(get)]
stock_wood: i64,
#[rune(get)]
stock_stone: i64,
#[rune(get)]
berry_near: bool,
#[rune(get)]
tree_near: bool,
#[rune(get)]
stone_near: bool,
#[rune(get)]
rabbit_near: bool,
#[rune(get)]
wolf_near: bool,
#[rune(get)]
prey_near: bool,
#[rune(get)]
roll: f64,
}
pub const DEFAULT_SCRIPT: &str = include_str!("behavior.rn");
/// Compiles the script and smoke-tests every entry point so errors surface at load time.
fn load_script(src: &str) -> Result<Vm, String> {
let mut module = Module::new();
module.ty::<Ctx>().map_err(|e| e.to_string())?;
let mut context = Context::with_default_modules().map_err(|e| e.to_string())?;
context.install(module).map_err(|e| e.to_string())?;
let mut sources = Sources::new();
sources
.insert(Source::new("behavior", src).map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
let mut diagnostics = Diagnostics::new();
let unit = rune::prepare(&mut sources)
.with_context(&context)
.with_diagnostics(&mut diagnostics)
.build();
let unit = match unit {
Ok(u) => u,
Err(_) => {
let mut buf = Buffer::no_color();
let _ = diagnostics.emit(&mut buf, &sources);
return Err(String::from_utf8_lossy(buf.as_slice()).into_owned());
}
};
let runtime = context.runtime().map_err(|e| e.to_string())?;
let mut vm = Vm::new(Arc::new(runtime), Arc::new(unit));
for f in ["person", "rabbit", "wolf"] {
let v = vm
.call([f], (Ctx::default(),))
.map_err(|e| format!("{f}(): {e}"))?;
rune::from_value::<String>(v).map_err(|_| format!("{f}() must return a string"))?;
}
Ok(vm)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Person {
#[serde(skip)]
actor_id: u64,
id: u32,
x: usize,
y: usize,
hunger: f32,
food: u32,
wood: u32,
stone: u32,
action: String,
dead: bool,
#[serde(default)]
age_ticks: u64,
#[serde(default)]
stage: LifeStage,
#[serde(default)]
stage_elapsed_ticks: u64,
#[serde(default = "default_stage_duration_days")]
stage_duration_days: u32,
#[serde(default)]
partner_id: Option<u32>,
}
fn default_stage_duration_days() -> u32 {
1
}
impl Person {
fn carry(&self) -> u32 {
self.food + self.wood + self.stone
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Animal {
#[serde(skip)]
actor_id: u64,
x: usize,
y: usize,
energy: f32,
dead: bool,
}
#[derive(Clone, Debug)]
enum EntityKind {
Person(Person),
Rabbit(Animal),
Wolf(Animal),
Partnership(Partnership),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Partnership {
id: u32,
first_person_id: u32,
second_person_id: u32,
formed_tick: u64,
last_birth_tick: u64,
#[serde(skip)]
actor_id: u64,
}
#[derive(Clone, Debug)]
struct EntityState {
id: u64,
kind: EntityKind,
}
#[derive(Debug)]
enum EntityMessage {
Read(SyncSender<EntityState>),
Write(EntityState, SyncSender<()>),
}
struct EntityActor {
state: EntityState,
}
impl ActorFactoryArgs<EntityState> for EntityActor {
fn create_args(state: EntityState) -> Self {
Self { state }
}
}
impl Actor for EntityActor {
type Msg = EntityMessage;
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, msg: Self::Msg, _sender: Sender) {
match msg {
EntityMessage::Read(reply) => {
let _ = reply.send(self.state.clone());
}
EntityMessage::Write(state, reply) => {
self.state = state;
let _ = reply.send(());
}
}
}
}
pub struct World {
seed: u64,
tiles: Vec<u8>,
res: Vec<(u8, u16)>,
people: Vec<Person>,
rabbits: Vec<Animal>,
wolves: Vec<Animal>,
partnerships: Vec<Partnership>,
next_partnership_id: u32,
actor_system: ActorSystem,
entities: HashMap<u64, ActorRef<EntityMessage>>,
next_actor_id: u64,
houses: Vec<(usize, usize)>,
home: (usize, usize),
food: u32,
wood: u32,
stone: u32,
tools: u32,
rng: Rng,
tick: u64,
next_id: u32,
log: Vec<String>,
script: String,
vm: Vm,
pub paused: bool,
pub speed: u32,
pub terrain_rev: u32,
calendar: CalendarSettings,
}
#[derive(Serialize, Deserialize)]
struct WorldSnapshot {
version: u32,
seed: Option<u64>,
tiles: Vec<u8>,
res: Vec<(u8, u16)>,
people: Vec<Person>,
rabbits: Vec<Animal>,
wolves: Vec<Animal>,
houses: Vec<(usize, usize)>,
home: (usize, usize),
food: u32,
wood: u32,
stone: u32,
tools: u32,
rng: u64,
tick: u64,
next_id: u32,
log: Vec<String>,
script: String,
paused: bool,
speed: u32,
terrain_rev: u32,
#[serde(default)]
calendar: CalendarSettings,
#[serde(default)]
partnerships: Vec<Partnership>,
#[serde(default = "default_next_partnership_id")]
next_partnership_id: u32,
}
fn default_next_partnership_id() -> u32 {
1
}
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
a.0.abs_diff(b.0).max(a.1.abs_diff(b.1))
}
impl World {
pub fn new(seed: u64) -> World {
let mut tiles = vec![0u8; W * H];
for y in 0..H {
for x in 0..W {
let dx = (x as f32 / W as f32 - 0.5) * 2.0;
let dy = (y as f32 / H as f32 - 0.5) * 2.0;
let d = (dx * dx + dy * dy).min(1.5);
let h = fbm(x as f32, y as f32, seed) + 0.25 - 0.55 * d;
let m = fbm(x as f32 + 500.0, y as f32 + 500.0, seed + 99);
tiles[y * W + x] = if h < 0.28 {
WATER_DEEP
} else if h < 0.36 {
WATER
} else if h < 0.41 {
SAND
} else if h < 0.68 {
if m > 0.52 { FOREST } else { GRASS }
} else if h < 0.78 {
HILLS
} else {
MOUNTAIN
};
}
}
let mut w = World {
seed,
tiles,
res: vec![(0, 0); W * H],
people: vec![],
rabbits: vec![],
wolves: vec![],
partnerships: vec![],
next_partnership_id: 1,
actor_system: ActorSystem::new().expect("failed to start Picard actor system"),
entities: HashMap::new(),
next_actor_id: 1,
houses: vec![],
home: (W / 2, H / 2),
food: STARTING_FOOD,
wood: 0,
stone: 0,
tools: 0,
rng: Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1),
tick: DEFAULT_TICKS_PER_DAY / 4,
next_id: 1,
log: vec![],
script: DEFAULT_SCRIPT.to_string(),
vm: load_script(DEFAULT_SCRIPT).expect("behavior.rn failed to load"),
paused: false,
speed: 1,
terrain_rev: 0,
calendar: CalendarSettings::default(),
};
w.home = w
.find_land(GRASS)
.unwrap_or_else(|| w.find_land(SAND).unwrap_or((W / 2, H / 2)));
for i in 0..W * H {
let r = w.rng.f();
w.res[i] = match w.tiles[i] {
FOREST if r < 0.6 => (TREE, 5),
GRASS if r < 0.04 => (BERRY, 4),
HILLS if r < 0.15 => (STONE, 6),
_ => (0, 0),
};
}
let (hx, hy) = w.home;
w.res[hy * W + hx] = (0, 0);
for i in 0..9 {
w.spawn_person();
let stage = if i < 3 {
LifeStage::YoungAdult
} else {
LifeStage::Adult
};
let duration_days = w.sample_stage_duration(stage);
let prior_stages = match stage {
LifeStage::YoungAdult => {
[LifeStage::Toddler, LifeStage::Child, LifeStage::Teen].as_slice()
}
LifeStage::Adult => [
LifeStage::Toddler,
LifeStage::Child,
LifeStage::Teen,
LifeStage::YoungAdult,
]
.as_slice(),
_ => &[],
};
let age_days: u64 = prior_stages
.iter()
.map(|&past_stage| w.sample_stage_duration(past_stage) as u64)
.sum();
let person = w.people.last_mut().expect("just spawned person");
person.stage = stage;
person.stage_duration_days = duration_days;
person.age_ticks = age_days * w.calendar.ticks_per_day;
}
for _ in 0..60 {
if let Some(p) = w.random_land() {
w.rabbits.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
for _ in 0..4 {
if let Some(p) = w.random_land() {
if cheb(p, w.home) > 15 {
w.wolves.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
}
w.sync_entities();
w
}
fn read_entities(actors: &HashMap<u64, ActorRef<EntityMessage>>) -> Vec<EntityState> {
let replies: Vec<_> = actors
.values()
.map(|actor| {
let (reply, result) = sync_channel(1);
actor.tell(EntityMessage::Read(reply), None);
result
})
.collect();
let mut states: Vec<_> = replies
.into_iter()
.map(|reply| {
reply
.recv_timeout(Duration::from_secs(5))
.expect("Picard entity actor did not respond to read")
})
.collect();
states.sort_by_key(|state| state.id);
states
}
fn load_entities(&mut self) {
let states = Self::read_entities(&self.entities);
self.people.clear();
self.rabbits.clear();
self.wolves.clear();
self.partnerships.clear();
for state in states {
match state.kind {
EntityKind::Person(person) => self.people.push(person),
EntityKind::Rabbit(animal) => self.rabbits.push(animal),
EntityKind::Wolf(animal) => self.wolves.push(animal),
EntityKind::Partnership(partnership) => self.partnerships.push(partnership),
}
}
}
fn sync_entities(&mut self) {
for person in &mut self.people {
if person.actor_id == 0 {
person.actor_id = self.next_actor_id;
self.next_actor_id += 1;
}
}
for animal in self.rabbits.iter_mut().chain(self.wolves.iter_mut()) {
if animal.actor_id == 0 {
animal.actor_id = self.next_actor_id;
self.next_actor_id += 1;
}
}
for partnership in &mut self.partnerships {
if partnership.actor_id == 0 {
partnership.actor_id = self.next_actor_id;
self.next_actor_id += 1;
}
}
let states = self
.people
.iter()
.cloned()
.map(|person| EntityState {
id: person.actor_id,
kind: EntityKind::Person(person),
})
.chain(self.rabbits.iter().cloned().map(|animal| EntityState {
id: animal.actor_id,
kind: EntityKind::Rabbit(animal),
}))
.chain(self.wolves.iter().cloned().map(|animal| EntityState {
id: animal.actor_id,
kind: EntityKind::Wolf(animal),
}))
.chain(
self.partnerships
.iter()
.cloned()
.map(|partnership| EntityState {
id: partnership.actor_id,
kind: EntityKind::Partnership(partnership),
}),
);
let mut active = HashSet::new();
for state in states {
active.insert(state.id);
if let Some(actor) = self.entities.get(&state.id) {
let (reply, result) = sync_channel(1);
actor.tell(EntityMessage::Write(state, reply), None);
result
.recv_timeout(Duration::from_secs(5))
.expect("Picard entity actor did not respond to write");
} else {
let id = state.id;
let name = format!("entity-{id}");
let actor = self
.actor_system
.actor_of_args::<EntityActor, _>(&name, state)
.expect("failed to create Picard entity actor");
self.entities.insert(id, actor);
}
}
self.entities.retain(|id, actor| {
if active.contains(id) {
true
} else {
actor.stop();
false
}
});
}
fn find_land(&self, kind: u8) -> Option<(usize, usize)> {
let c = (W / 2, H / 2);
let mut best: Option<(usize, usize)> = None;
for y in 0..H {
for x in 0..W {
if self.tiles[y * W + x] == kind
&& best.is_none_or(|b| cheb((x, y), c) < cheb(b, c))
{
best = Some((x, y));
}
}
}
best
}
fn random_land(&mut self) -> Option<(usize, usize)> {
for _ in 0..50 {
let (x, y) = (self.rng.below(W), self.rng.below(H));
if self.walkable(x as i32, y as i32) {
return Some((x, y));
}
}
None
}
fn spawn_person(&mut self) {
self.spawn_person_at(self.home);
}
fn spawn_person_at(&mut self, (x, y): (usize, usize)) {
let stage_duration_days = self.sample_stage_duration(LifeStage::Toddler);
self.people.push(Person {
actor_id: 0,
id: self.next_id,
x,
y,
hunger: 10.0,
food: 0,
wood: 0,
stone: 0,
action: "idle".to_string(),
dead: false,
age_ticks: 0,
stage: LifeStage::Toddler,
stage_elapsed_ticks: 0,
stage_duration_days,
partner_id: None,
});
self.next_id += 1;
}
fn sample_stage_duration(&mut self, stage: LifeStage) -> u32 {
let duration = match stage {
LifeStage::Toddler => self.calendar.toddler,
LifeStage::Child => self.calendar.child,
LifeStage::Teen => self.calendar.teen,
LifeStage::YoungAdult => self.calendar.young_adult,
LifeStage::Adult => self.calendar.adult,
LifeStage::OlderAdult => self.calendar.older_adult,
LifeStage::Elderly => self.calendar.elderly,
LifeStage::Dead => StageDuration {
min_days: 1,
max_days: 1,
fertile: false,
},
};
let range = duration.max_days - duration.min_days + 1;
duration.min_days + self.rng.below(range as usize) as u32
}
fn say(&mut self, s: String) {
self.log.push(format!(
"[day {}] {}",
self.tick / self.calendar.ticks_per_day + 1,
s
));
if self.log.len() > 8 {
self.log.remove(0);
}
}
pub fn daylight(&self) -> f32 {
let ticks_per_day = self.calendar.ticks_per_day;
let phase = (self.tick % ticks_per_day) as f32 / ticks_per_day as f32;
let v = -(phase * std::f32::consts::TAU).cos();
(0.5 + v * 0.8).clamp(0.0, 1.0)
}
fn walkable(&self, x: i32, y: i32) -> bool {
if x < 0 || y < 0 || x >= W as i32 || y >= H as i32 {
return false;
}
matches!(
self.tiles[y as usize * W + x as usize],
SAND | GRASS | FOREST | HILLS
)
}
fn move_dir(&mut self, x: usize, y: usize, dx: i32, dy: i32) -> (usize, usize) {
for (a, b) in [(dx, dy), (dx, 0), (0, dy)] {
if (a, b) != (0, 0) && self.walkable(x as i32 + a, y as i32 + b) {
return ((x as i32 + a) as usize, (y as i32 + b) as usize);
}
}
self.wander(x, y)
}
fn wander(&mut self, x: usize, y: usize) -> (usize, usize) {
for _ in 0..4 {
let (a, b) = (self.rng.below(3) as i32 - 1, self.rng.below(3) as i32 - 1);
if (a, b) != (0, 0) && self.walkable(x as i32 + a, y as i32 + b) {
return ((x as i32 + a) as usize, (y as i32 + b) as usize);
}
}
(x, y)
}
fn move_toward(&mut self, from: (usize, usize), to: (usize, usize)) -> (usize, usize) {
if from == to {
return from;
}
let start = from.1 * W + from.0;
let target = to.1 * W + to.0;
let mut frontier = BinaryHeap::from([Reverse((cheb(from, to), start))]);
let mut costs = vec![usize::MAX; W * H];
let mut previous = vec![usize::MAX; W * H];
costs[start] = 0;
previous[start] = start;
while let Some(Reverse((_, current))) = frontier.pop() {
if current == target {
let mut next = target;
while previous[next] != start {
next = previous[next];
}
return (next % W, next / W);
}
let position = (current % W, current / W);
for dy in -1..=1 {
for dx in -1..=1 {
if (dx, dy) == (0, 0) {
continue;
}
let x = position.0 as i32 + dx;
let y = position.1 as i32 + dy;
if !self.walkable(x, y) {
continue;
}
let neighbor = y as usize * W + x as usize;
let cost = costs[current] + 1;
if cost >= costs[neighbor] {
continue;
}
costs[neighbor] = cost;
previous[neighbor] = current;
frontier.push(Reverse((
cost + cheb((x as usize, y as usize), to),
neighbor,
)));
}
}
}
self.wander(from.0, from.1)
}
fn nearest_res(&self, from: (usize, usize), kind: u8, r: usize) -> Option<(usize, usize)> {
let mut best: Option<(usize, usize)> = None;
let (x0, x1) = (from.0.saturating_sub(r), (from.0 + r).min(W - 1));
let (y0, y1) = (from.1.saturating_sub(r), (from.1 + r).min(H - 1));
for y in y0..=y1 {
for x in x0..=x1 {
if self.res[y * W + x].0 == kind
&& best.is_none_or(|b| cheb((x, y), from) < cheb(b, from))
{
best = Some((x, y));
}
}
}
best
}
fn nearest_animal(list: &[Animal], from: (usize, usize), r: usize) -> Option<usize> {
let mut best: Option<(usize, usize)> = None;
for (i, a) in list.iter().enumerate() {
let d = cheb((a.x, a.y), from);
if !a.dead && d <= r && best.is_none_or(|(_, bd)| d < bd) {
best = Some((i, d));
}
}
best.map(|b| b.0)
}
pub fn step(&mut self) {
self.load_entities();
self.tick += 1;
self.regrow();
self.advance_people();
if self.tick % 2 == 0 {
for i in 0..self.people.len() {
self.tick_person(i);
}
for i in 0..self.rabbits.len() {
self.tick_rabbit(i);
}
} else {
for i in 0..self.wolves.len() {
self.tick_wolf(i);
}
}
self.people.retain(|p| !p.dead);
self.rabbits.retain(|a| !a.dead);
self.wolves.retain(|a| !a.dead);
self.prune_partnerships();
self.population();
self.sync_entities();
}
fn advance_people(&mut self) {
let mut notices = Vec::new();
for i in 0..self.people.len() {
if self.people[i].dead {
continue;
}
self.people[i].age_ticks = self.people[i].age_ticks.saturating_add(1);
self.people[i].stage_elapsed_ticks =
self.people[i].stage_elapsed_ticks.saturating_add(1);
let duration_ticks =
self.people[i].stage_duration_days as u64 * self.calendar.ticks_per_day;
if self.people[i].stage_elapsed_ticks < duration_ticks {
continue;
}
let person_id = self.people[i].id;
if let Some(next) = self.people[i].stage.next() {
self.people[i].stage = next;
self.people[i].stage_elapsed_ticks = 0;
if next == LifeStage::Dead {
self.people[i].dead = true;
notices.push(format!("Person #{person_id} died of old age"));
} else {
self.people[i].stage_duration_days = self.sample_stage_duration(next);
notices.push(format!(
"Person #{person_id} became a {}",
next.key().replace('_', " ")
));
}
} else {
self.people[i].dead = true;
notices.push(format!("Person #{person_id} died of old age"));
}
}
for notice in notices {
self.say(notice);
}
}
fn prune_partnerships(&mut self) {
let alive: HashSet<u32> = self.people.iter().map(|person| person.id).collect();
self.partnerships.retain(|partnership| {
alive.contains(&partnership.first_person_id)
&& alive.contains(&partnership.second_person_id)
});
let partnered: HashSet<(u32, u32)> = self
.partnerships
.iter()
.map(|partnership| (partnership.first_person_id, partnership.second_person_id))
.flat_map(|(first, second)| [(first, second), (second, first)])
.collect();
for person in &mut self.people {
if person
.partner_id
.is_some_and(|partner| !partnered.contains(&(person.id, partner)))
{
person.partner_id = None;
}
}
}
fn regrow(&mut self) {
for _ in 0..20 {
let i = self.rng.below(W * H);
if self.res[i].0 != 0 {
continue;
}
let p = self.rng.f();
match self.tiles[i] {
FOREST if p < 0.02 => self.res[i] = (TREE, 5),
GRASS if p < 0.02 => self.res[i] = (BERRY, 4),
HILLS if p < 0.004 => self.res[i] = (STONE, 6),
_ => {}
}
}
}
fn population(&mut self) {
let ticks_per_day = self.calendar.ticks_per_day;
if self.tick.is_multiple_of(ticks_per_day) {
self.find_partners();
}
let ticks_per_week = ticks_per_day * self.calendar.days_per_week as u64;
if self.tick.is_multiple_of(ticks_per_week) {
self.have_children(ticks_per_week);
}
if self.tick % 50 == 0 {
if self.rabbits.len() < 15 {
if let Some(p) = self.random_land() {
self.rabbits.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
if self.wolves.len() < 2 && self.rabbits.len() > 40 {
if let Some(p) = self.random_land() {
if cheb(p, self.home) > 15 {
self.wolves.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
dead: false,
});
}
}
}
}
}
fn find_partners(&mut self) {
let ticks_per_week = self.calendar.ticks_per_day * self.calendar.days_per_week as u64;
for first in 0..self.people.len() {
if self.people[first].partner_id.is_some()
|| !self.people[first].stage.can_partner()
|| self.people[first].dead
{
continue;
}
let first_position = (self.people[first].x, self.people[first].y);
let second = (0..self.people.len())
.filter(|&index| {
index != first
&& self.people[index].partner_id.is_none()
&& self.people[index].stage.can_partner()
&& !self.people[index].dead
})
.filter_map(|index| {
let distance =
cheb(first_position, (self.people[index].x, self.people[index].y));
(distance <= 12).then_some((index, distance))
})
.min_by_key(|(_, distance)| *distance)
.map(|(index, _)| index);
let Some(second) = second else {
continue;
};
let first_id = self.people[first].id;
let second_id = self.people[second].id;
self.people[first].partner_id = Some(second_id);
self.people[second].partner_id = Some(first_id);
self.partnerships.push(Partnership {
id: self.next_partnership_id,
first_person_id: first_id,
second_person_id: second_id,
formed_tick: self.tick,
last_birth_tick: self.tick / ticks_per_week * ticks_per_week,
actor_id: 0,
});
self.next_partnership_id += 1;
self.say(format!(
"People #{first_id} and #{second_id} became partners"
));
}
}
fn have_children(&mut self, birth_interval_ticks: u64) {
if self.food < 10 || self.people.len() >= self.population_capacity() {
return;
}
let partnerships: Vec<_> = self
.partnerships
.iter()
.map(|partnership| {
(
partnership.id,
partnership.first_person_id,
partnership.second_person_id,
partnership.last_birth_tick,
)
})
.collect();
for (partnership_id, first_id, second_id, last_birth_tick) in partnerships {
if self.food < 10 || self.people.len() >= self.population_capacity() {
break;
}
if self.tick.saturating_sub(last_birth_tick) < birth_interval_ticks {
continue;
}
let Some(first_index) = self.people.iter().position(|p| p.id == first_id) else {
continue;
};
let Some(second_index) = self.people.iter().position(|p| p.id == second_id) else {
continue;
};
let first_stage = self.people[first_index].stage;
let second_stage = self.people[second_index].stage;
let can_have_children = self
.calendar
.duration(first_stage.key())
.is_some_and(|duration| duration.fertile)
&& self
.calendar
.duration(second_stage.key())
.is_some_and(|duration| duration.fertile);
if !can_have_children {
continue;
}
self.food -= 6;
self.spawn_person_at(self.home);
if let Some(partnership) = self
.partnerships
.iter_mut()
.find(|partnership| partnership.id == partnership_id)
{
partnership.last_birth_tick = self.tick;
}
self.say(format!(
"People #{first_id} and #{second_id} welcomed a toddler"
));
}
}
fn population_capacity(&self) -> usize {
BASE_POPULATION_CAPACITY + self.houses.len() * 3
}
fn decide(&mut self, func: &str, c: Ctx) -> String {
match self.vm.call([func], (c,)) {
Ok(v) => rune::from_value::<String>(v).unwrap_or_else(|_| "wander".into()),
Err(e) => {
eprintln!("script error in {func}: {e}");
"wander".into()
}
}
}
fn tick_person(&mut self, i: usize) {
let mut p = self.people[i].clone();
if p.dead {
return;
}
let night = self.daylight() < 0.2;
let pos = (p.x, p.y);
let home = self.home;
let at_home = cheb(pos, home) <= 1;
p.hunger += if night && at_home { 0.01 } else { 0.02 };
if at_home {
self.food += p.food;
self.wood += p.wood;
self.stone += p.stone;
p.food = 0;
p.wood = 0;
p.stone = 0;
if p.hunger > 40.0 && self.food > 0 {
self.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
self.craft_and_build();
}
if p.hunger > 50.0 && p.food > 0 {
p.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
if p.hunger >= 100.0 {
p.dead = true;
self.say(format!("Person #{} starved", p.id));
self.people[i] = p;
return;
}
if p.hunger > 40.0 && self.food > 0 && !at_home {
(p.x, p.y) = self.move_toward(pos, home);
p.action = "returning for food".to_string();
self.people[i] = p;
return;
}
if !p.stage.can_partner() {
if !at_home {
(p.x, p.y) = self.move_toward(pos, home);
}
p.action = "growing up".to_string();
self.people[i] = p;
return;
}
let berry = self.nearest_res(pos, BERRY, 25);
let tree = self.nearest_res(pos, TREE, 25);
let stone = self.nearest_res(pos, STONE, 25);
let rabbit = Self::nearest_animal(&self.rabbits, pos, 25);
let c = Ctx {
night,
at_home,
hunger: p.hunger as f64,
carry: p.carry() as i64,
carried_food: p.food as i64,
stock_food: self.food as i64,
stock_wood: self.wood as i64,
stock_stone: self.stone as i64,
berry_near: berry.is_some(),
tree_near: tree.is_some(),
stone_near: stone.is_some(),
rabbit_near: rabbit.is_some(),
roll: self.rng.f() as f64,
..Default::default()
};
let choice = self.decide("person", c);
let yield_n: u16 = if self.tools >= self.people.len() as u32 {
2
} else {
1
};
let gather = match choice.as_str() {
"forage" => Some((BERRY, berry, "foraging")),
"chop" => Some((TREE, tree, "chopping wood")),
"mine" => Some((STONE, stone, "mining stone")),
_ => None,
};
match (choice.as_str(), gather, rabbit) {
("sleep", ..) => p.action = "sleeping".to_string(),
("go_home" | "haul" | "eat", ..) => {
p.action = match choice.as_str() {
"go_home" => "going home",
"haul" => "hauling goods",
_ => "going to eat",
}
.to_string();
if !at_home {
(p.x, p.y) = self.move_toward(pos, home);
}
}
("hunt", _, Some(r)) => {
p.action = "hunting".to_string();
let t = (self.rabbits[r].x, self.rabbits[r].y);
if cheb(pos, t) <= 1 {
if self.rng.chance(0.5) {
self.rabbits[r].dead = true;
p.food += 3;
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
}
(_, Some((kind, Some(t), label)), _) => {
p.action = label.to_string();
let idx = pos.1 * W + pos.0;
if pos == t && self.res[idx].0 == kind {
let take = yield_n.min(self.res[idx].1);
self.res[idx].1 -= take;
if self.res[idx].1 == 0 {
self.res[idx].0 = 0;
}
match kind {
BERRY => p.food += take as u32,
TREE => p.wood += take as u32,
_ => p.stone += take as u32,
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
}
_ => {
p.action = "wandering".to_string();
(p.x, p.y) = self.wander(p.x, p.y);
}
}
self.people[i] = p;
}
fn craft_and_build(&mut self) {
if self.wood >= 3 && self.stone >= 1 && self.tools < self.people.len() as u32 {
self.wood -= 3;
self.stone -= 1;
self.tools += 1;
}
if self.wood >= 12 && self.stone >= 4 && self.houses.len() < 10 {
let (hx, hy) = self.home;
let x = (hx as i32 + self.rng.below(13) as i32 - 6).clamp(0, W as i32 - 1);
let y = (hy as i32 + self.rng.below(13) as i32 - 6).clamp(0, H as i32 - 1);
if self.walkable(x, y)
&& self.res[y as usize * W + x as usize].0 == 0
&& !self.houses.contains(&(x as usize, y as usize))
{
self.wood -= 12;
self.stone -= 4;
self.houses.push((x as usize, y as usize));
self.say("A new house was built".into());
}
}
}
fn tick_rabbit(&mut self, i: usize) {
let mut a = self.rabbits[i].clone();
a.energy -= 0.05;
if matches!(self.tiles[a.y * W + a.x], GRASS | FOREST) {
a.energy = (a.energy + 0.2).min(100.0);
}
let pos = (a.x, a.y);
let wolf = Self::nearest_animal(&self.wolves, pos, 4);
let c = Ctx {
wolf_near: wolf.is_some(),
roll: self.rng.f() as f64,
..Default::default()
};
match (self.decide("rabbit", c).as_str(), wolf) {
("flee", Some(w)) => {
let dx = (a.x as i32 - self.wolves[w].x as i32).signum();
let dy = (a.y as i32 - self.wolves[w].y as i32).signum();
(a.x, a.y) = self.move_dir(a.x, a.y, dx, dy);
}
("wander", _) => (a.x, a.y) = self.wander(a.x, a.y),
_ => {}
}
if a.energy > 80.0 && self.rabbits.len() < 120 && self.rng.chance(0.01) {
a.energy -= 30.0;
self.rabbits.push(Animal {
actor_id: 0,
x: a.x,
y: a.y,
energy: 50.0,
dead: false,
});
}
if a.energy <= 0.0 {
a.dead = true;
}
self.rabbits[i] = a;
}
fn tick_wolf(&mut self, i: usize) {
let mut a = self.wolves[i].clone();
a.energy -= 0.15;
let pos = (a.x, a.y);
let prey = Self::nearest_animal(&self.rabbits, pos, 10);
let c = Ctx {
prey_near: prey.is_some(),
..Default::default()
};
match (self.decide("wolf", c).as_str(), prey) {
("hunt", Some(r)) => {
let t = (self.rabbits[r].x, self.rabbits[r].y);
if cheb(pos, t) <= 1 {
if self.rng.chance(0.4) {
self.rabbits[r].dead = true;
a.energy = (a.energy + 50.0).min(100.0);
}
} else {
(a.x, a.y) = self.move_toward(pos, t);
}
}
_ => (a.x, a.y) = self.wander(a.x, a.y),
}
if a.energy > 90.0 && self.wolves.len() < 10 && self.rng.chance(0.02) {
a.energy -= 40.0;
self.wolves.push(Animal {
actor_id: 0,
x: a.x,
y: a.y,
energy: 50.0,
dead: false,
});
}
if a.energy <= 0.0 {
a.dead = true;
}
self.wolves[i] = a;
}
pub fn set_script(&mut self, src: &str) -> Result<(), String> {
let vm = load_script(src)?;
self.vm = vm;
self.script = src.to_string();
Ok(())
}
pub fn restarted(&self, seed: u64) -> Self {
let mut world = Self::new(seed);
world.speed = self.speed;
world.paused = self.paused;
world.calendar = self.calendar.clone();
world.calendar.normalize();
world.tick = world.calendar.ticks_per_day / 4;
if self.script != DEFAULT_SCRIPT {
world
.set_script(&self.script)
.expect("saved behavior script failed to reload");
}
world
}
pub fn regenerated_seed(&self) -> u64 {
let mut seed = rand::random::<u64>();
if seed == self.seed {
seed = seed.wrapping_add(1);
}
seed
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn snapshot_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&WorldSnapshot {
version: 1,
seed: Some(self.seed),
tiles: self.tiles.clone(),
res: self.res.clone(),
people: self.people.clone(),
rabbits: self.rabbits.clone(),
wolves: self.wolves.clone(),
houses: self.houses.clone(),
home: self.home,
food: self.food,
wood: self.wood,
stone: self.stone,
tools: self.tools,
rng: self.rng.0,
tick: self.tick,
next_id: self.next_id,
log: self.log.clone(),
script: self.script.clone(),
paused: self.paused,
speed: self.speed,
terrain_rev: self.terrain_rev,
calendar: self.calendar.clone(),
partnerships: self.partnerships.clone(),
next_partnership_id: self.next_partnership_id,
})
}
pub fn script(&self) -> &str {
&self.script
}
pub fn from_snapshot(snapshot: &str, fallback_seed: u64) -> Result<Self, String> {
let snapshot_value: serde_json::Value =
serde_json::from_str(snapshot).map_err(|e| e.to_string())?;
let legacy_people = snapshot_value
.get("people")
.and_then(serde_json::Value::as_array)
.is_some_and(|people| {
!people.is_empty()
&& people.iter().all(|person| {
person
.as_object()
.is_some_and(|person| !person.contains_key("stage"))
})
});
let saved: WorldSnapshot =
serde_json::from_value(snapshot_value).map_err(|e| e.to_string())?;
if saved.version != 1 {
return Err(format!(
"unsupported world snapshot version {}",
saved.version
));
}
if saved.tiles.len() != W * H || saved.res.len() != W * H {
return Err("world snapshot has invalid terrain dimensions".into());
}
let vm = load_script(&saved.script)?;
let mut world = Self {
seed: saved.seed.unwrap_or(fallback_seed),
tiles: saved.tiles,
res: saved.res,
people: saved.people,
rabbits: saved.rabbits,
wolves: saved.wolves,
partnerships: saved.partnerships,
next_partnership_id: saved.next_partnership_id,
actor_system: ActorSystem::new()
.map_err(|e| format!("failed to start Picard actor system: {e}"))?,
entities: HashMap::new(),
next_actor_id: 1,
houses: saved.houses,
home: saved.home,
food: saved.food,
wood: saved.wood,
stone: saved.stone,
tools: saved.tools,
rng: Rng(saved.rng),
tick: saved.tick,
next_id: saved.next_id,
log: saved.log,
script: saved.script,
vm,
paused: saved.paused,
speed: saved.speed,
terrain_rev: saved.terrain_rev,
calendar: saved.calendar,
};
world.calendar.normalize();
if legacy_people {
world.initialize_legacy_people();
}
world.prune_partnerships();
world.sync_entities();
Ok(world)
}
fn initialize_legacy_people(&mut self) {
for index in 0..self.people.len() {
let stage = if index < 3 {
LifeStage::YoungAdult
} else {
LifeStage::Adult
};
let duration_days = self.sample_stage_duration(stage);
let prior_stages = match stage {
LifeStage::YoungAdult => {
[LifeStage::Toddler, LifeStage::Child, LifeStage::Teen].as_slice()
}
LifeStage::Adult => [
LifeStage::Toddler,
LifeStage::Child,
LifeStage::Teen,
LifeStage::YoungAdult,
]
.as_slice(),
_ => &[],
};
let age_days: u64 = prior_stages
.iter()
.map(|&past_stage| self.sample_stage_duration(past_stage) as u64)
.sum();
let person = &mut self.people[index];
person.stage = stage;
person.stage_duration_days = duration_days;
person.stage_elapsed_ticks = 0;
person.age_ticks = age_days * self.calendar.ticks_per_day;
}
}
fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> {
let n: Vec<i64> = (i..i + 4).filter_map(|k| t.get(k)?.parse().ok()).collect();
if n.len() != 4 {
return None;
}
let c = |v: i64, max: usize| v.clamp(0, max as i64 - 1) as usize;
let (x0, x1) = (c(n[0].min(n[2]), W), c(n[0].max(n[2]), W));
let (y0, y1) = (c(n[1].min(n[3]), H), c(n[1].max(n[3]), H));
Some((x0, y0, x1, y1))
}
/// Applies one whitespace-separated edit command; malformed commands are ignored.
pub fn apply(&mut self, cmd: &str) {
self.load_entities();
let t: Vec<&str> = cmd.split_whitespace().collect();
let num = |i: usize| t.get(i).and_then(|s| s.parse::<i64>().ok());
match t.first().copied() {
Some("pause") => self.paused = num(1) != Some(0),
Some("calendar") => {
if let (Some(setting), Some(value)) = (t.get(1), num(2)) {
let value = value.clamp(1, 1_000_000) as u64;
match *setting {
"ticks_per_day" => self.calendar.ticks_per_day = value,
"days_per_week" => self.calendar.days_per_week = value.min(100) as u32,
"weeks_per_month" => self.calendar.weeks_per_month = value.min(100) as u32,
"months_per_year" => self.calendar.months_per_year = value.min(100) as u32,
_ => {}
}
}
}
Some("stage") => {
if let (Some(stage), Some(property)) = (t.get(1), t.get(2)) {
if *property == "fertile" {
if let Some(value) = num(3) {
if let Some(range) = self.calendar.duration_mut(stage) {
range.fertile = value != 0;
}
}
} else if let (Some(min_days), Some(max_days)) = (num(2), num(3)) {
let min_days = min_days.clamp(1, 1_000_000) as u32;
let max_days = max_days.clamp(1, 1_000_000) as u32;
if min_days <= max_days {
if let Some(range) = self.calendar.duration_mut(stage) {
range.min_days = min_days;
range.max_days = max_days;
}
}
}
}
}
Some("speed") => {
if let Some(n) = num(1) {
self.speed = n.clamp(1, 8) as u32;
}
}
Some("stock") => {
if let (Some(name), Some(v)) = (t.get(1), num(2)) {
let v = v.clamp(0, 9999) as u32;
match *name {
"food" => self.food = v,
"wood" => self.wood = v,
"stone" => self.stone = v,
"tools" => self.tools = v,
_ => {}
}
}
}
Some("person") => {
if let (Some(id), Some(field), Some(v)) = (num(1), t.get(2), num(3)) {
if let Some(p) = self.people.iter_mut().find(|p| p.id as i64 == id) {
let u = v.clamp(0, 99) as u32;
match *field {
"hunger" => p.hunger = v.clamp(0, 99) as f32,
"food" => p.food = u,
"wood" => p.wood = u,
"stone" => p.stone = u,
_ => {}
}
}
}
}
Some("energy") => {
if let (Some((x0, y0, x1, y1)), Some(v)) = (Self::rect(&t, 1), num(5)) {
let v = v.clamp(1, 100) as f32;
let inside = |a: &Animal| a.x >= x0 && a.x <= x1 && a.y >= y0 && a.y <= y1;
for a in self.rabbits.iter_mut().chain(self.wolves.iter_mut()) {
if inside(a) {
a.energy = v;
}
}
}
}
Some("terrain") => {
if let (Some((x0, y0, x1, y1)), Some(kind)) = (Self::rect(&t, 1), num(5)) {
if (0..=6).contains(&kind) {
self.paint_terrain(x0, y0, x1, y1, kind as u8);
}
}
}
Some("res") => {
if let (Some((x0, y0, x1, y1)), Some(kind), Some(amt)) =
(Self::rect(&t, 1), num(5), num(6))
{
if (0..=3).contains(&kind) {
let amt = amt.clamp(1, 50) as u16;
for y in y0..=y1 {
for x in x0..=x1 {
let i = y * W + x;
let free = self.walkable(x as i32, y as i32)
&& (x, y) != self.home
&& !self.houses.contains(&(x, y));
if kind == 0 {
self.res[i] = (0, 0);
} else if free {
self.res[i] = (kind as u8, amt);
}
}
}
}
}
}
Some("spawn") => {
if let (Some(what), Some((x0, y0, x1, y1))) = (t.get(1), Self::rect(&t, 2)) {
let mut left = 25;
for y in y0..=y1 {
for x in x0..=x1 {
if left == 0 || !self.walkable(x as i32, y as i32) {
continue;
}
left -= 1;
let a = Animal {
actor_id: 0,
x,
y,
energy: 70.0,
dead: false,
};
match *what {
"person" => {
self.spawn_person();
let p = self.people.last_mut().unwrap();
(p.x, p.y) = (x, y);
}
"rabbit" => self.rabbits.push(a),
"wolf" => self.wolves.push(a),
_ => left += 1,
}
}
}
}
}
Some("house") => {
if let Some((x0, y0, x1, y1)) = Self::rect(&t, 1) {
for y in y0..=y1 {
for x in x0..=x1 {
if self.houses.len() < 30
&& self.walkable(x as i32, y as i32)
&& self.res[y * W + x].0 == 0
&& (x, y) != self.home
&& !self.houses.contains(&(x, y))
{
self.houses.push((x, y));
}
}
}
}
}
Some("remove") => {
if let (Some((x0, y0, x1, y1)), Some(what)) = (Self::rect(&t, 1), t.get(5)) {
let inside = |x: usize, y: usize| x >= x0 && x <= x1 && y >= y0 && y <= y1;
let all = *what == "all";
if all || *what == "people" {
self.people.retain(|p| !inside(p.x, p.y));
}
if all || *what == "rabbits" {
self.rabbits.retain(|a| !inside(a.x, a.y));
}
if all || *what == "wolves" {
self.wolves.retain(|a| !inside(a.x, a.y));
}
if all || *what == "houses" {
self.houses.retain(|h| !inside(h.0, h.1));
}
if all || *what == "res" {
for y in y0..=y1 {
for x in x0..=x1 {
self.res[y * W + x] = (0, 0);
}
}
}
}
}
_ => {}
}
self.prune_partnerships();
self.sync_entities();
}
fn paint_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, kind: u8) {
let land = matches!(kind, SAND | GRASS | FOREST | HILLS);
for y in y0..=y1 {
for x in x0..=x1 {
if !land && (x, y) == self.home {
continue;
}
self.tiles[y * W + x] = kind;
if !land {
self.res[y * W + x] = (0, 0);
self.houses.retain(|h| *h != (x, y));
}
}
}
if !land {
let ok = |tiles: &[u8], x: usize, y: usize| {
matches!(tiles[y * W + x], SAND | GRASS | FOREST | HILLS)
};
let tiles = self.tiles.clone();
self.rabbits.retain(|a| ok(&tiles, a.x, a.y));
self.wolves.retain(|a| ok(&tiles, a.x, a.y));
let home = self.home;
for p in self.people.iter_mut().filter(|p| !ok(&tiles, p.x, p.y)) {
(p.x, p.y) = home;
}
}
self.terrain_rev += 1;
}
pub fn terrain_json(&self) -> String {
let tiles: String = self.tiles.iter().map(|t| (b'0' + t) as char).collect();
format!("{{\"w\":{},\"h\":{},\"tiles\":\"{}\"}}", W, H, tiles)
}
pub fn state_json(&self) -> String {
let mut s = String::new();
let days_per_month =
self.calendar.days_per_week as u64 * self.calendar.weeks_per_month as u64;
let days_per_year = days_per_month * self.calendar.months_per_year as u64;
let day = self.tick / self.calendar.ticks_per_day;
let year = day / days_per_year + 1;
let month = (day / days_per_month) % self.calendar.months_per_year as u64 + 1;
let week =
(day / self.calendar.days_per_week as u64) % self.calendar.weeks_per_month as u64 + 1;
let date_day = day % self.calendar.days_per_week as u64 + 1;
let _ = write!(
s,
"{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
self.paused,
self.speed,
self.seed,
self.terrain_rev,
self.tick,
day + 1,
year,
month,
week,
date_day,
(self.tick % self.calendar.ticks_per_day) as f32 / self.calendar.ticks_per_day as f32
* 24.0,
self.daylight(),
self.food,
self.wood,
self.stone,
self.tools,
self.home.0,
self.home.1
);
let _ = write!(
s,
"\"calendar\":{{\"ticks_per_day\":{},\"days_per_week\":{},\"weeks_per_month\":{},\"months_per_year\":{},",
self.calendar.ticks_per_day,
self.calendar.days_per_week,
self.calendar.weeks_per_month,
self.calendar.months_per_year
);
for (index, (name, range)) in [
("toddler", self.calendar.toddler),
("child", self.calendar.child),
("teen", self.calendar.teen),
("young_adult", self.calendar.young_adult),
("adult", self.calendar.adult),
("older_adult", self.calendar.older_adult),
("elderly", self.calendar.elderly),
]
.into_iter()
.enumerate()
{
if index > 0 {
s.push(',');
}
let _ = write!(
s,
"\"{name}\":{{\"min_days\":{},\"max_days\":{},\"fertile\":{}}}",
range.min_days, range.max_days, range.fertile
);
}
s.push_str("},");
s.push_str("\"houses\":[");
let j: Vec<String> = self
.houses
.iter()
.map(|h| format!("[{},{}]", h.0, h.1))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"people\":[");
let j: Vec<String> = self
.people
.iter()
.map(|p| {
format!(
"[{},{},{},\"{}\",{:.0},{},{},{},\"{}\",{},{}]",
p.id,
p.x,
p.y,
p.action,
p.hunger,
p.food,
p.wood,
p.stone,
p.stage.key(),
p.age_ticks / self.calendar.ticks_per_day,
p.partner_id
.map_or_else(|| "null".to_string(), |partner| partner.to_string())
)
})
.collect();
s.push_str(&j.join(","));
s.push_str("],\"rabbits\":[");
let j: Vec<String> = self
.rabbits
.iter()
.map(|a| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"wolves\":[");
let j: Vec<String> = self
.wolves
.iter()
.map(|a| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
.collect();
s.push_str(&j.join(","));
s.push_str("],\"res\":[");
let mut first = true;
for (i, r) in self.res.iter().enumerate() {
if r.0 != 0 {
if !first {
s.push(',');
}
first = false;
let _ = write!(s, "[{},{},{},{}]", i % W, i / W, r.0, r.1);
}
}
s.push_str("],\"log\":[");
let j: Vec<String> = self.log.iter().map(|l| format!("\"{}\"", l)).collect();
s.push_str(&j.join(","));
s.push_str("]}");
s
}
}
impl Drop for World {
fn drop(&mut self) {
let _ = self.actor_system.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, H, LifeStage, W, World, cheb};
#[test]
fn new_people_progress_through_every_stage_and_die() {
let mut world = World::new(42);
world.calendar.ticks_per_day = 1;
for stage in [
"toddler",
"child",
"teen",
"young_adult",
"adult",
"older_adult",
"elderly",
] {
let duration = world.calendar.duration_mut(stage).unwrap();
duration.min_days = 1;
duration.max_days = 1;
}
world.spawn_person();
let index = world.people.len() - 1;
assert_eq!(world.people[index].stage, LifeStage::Toddler);
for expected in [
LifeStage::Child,
LifeStage::Teen,
LifeStage::YoungAdult,
LifeStage::Adult,
LifeStage::OlderAdult,
LifeStage::Elderly,
LifeStage::Dead,
] {
world.advance_people();
assert_eq!(world.people[index].stage, expected);
}
assert!(world.people[index].dead);
}
#[test]
fn eligible_people_form_partnership_actor_and_have_toddlers() {
let mut world = World::new(42);
world.calendar.ticks_per_day = 1;
world.calendar.days_per_week = 1;
world.food = 1000;
world.find_partners();
assert_eq!(world.partnerships.len(), 4);
assert_eq!(
world
.people
.iter()
.filter(|person| person.partner_id.is_some())
.count(),
8
);
world.people[0].x = 0;
world.people[0].y = 0;
world.sync_entities();
assert_eq!(
world.entities.len(),
world.people.len() + world.rabbits.len() + world.wolves.len() + 4
);
world.load_entities();
assert_eq!(world.partnerships.len(), 4);
let initial_population = world.people.len();
while world.people.len() < world.population_capacity() {
world.tick += 1;
world.have_children(1);
}
assert_eq!(world.people.len(), world.population_capacity());
assert_eq!(world.people.len() - initial_population, 3);
let toddler = world
.people
.iter()
.find(|person| person.stage == LifeStage::Toddler)
.expect("fertile partner actors should eventually have a child");
assert_eq!(toddler.age_ticks, 0);
assert_eq!((toddler.x, toddler.y), world.home);
}
#[test]
fn each_fertile_partnership_can_have_a_weekly_birth() {
let mut world = World::new(42);
world.calendar.ticks_per_day = 1;
world.food = 1000;
world.houses.extend([(0, 0), (1, 0)]);
world.find_partners();
world.tick += 7;
world.have_children(7);
assert_eq!(world.people.len(), 13);
assert_eq!(
world
.people
.iter()
.filter(|person| person.stage == LifeStage::Toddler)
.count(),
4
);
}
#[test]
fn people_can_pathfind_back_home_around_blocked_terrain() {
let mut world = World::new(42);
let start = (59, 48);
let mut position = start;
for _ in 0..W.max(H) * 2 {
if position == world.home {
break;
}
position = world.move_toward(position, world.home);
}
assert_eq!(position, world.home);
assert!(world.walkable(start.0 as i32, start.1 as i32));
}
#[test]
fn village_survives_until_its_first_weekly_birth_check() {
let mut world = World::new(42);
let first_week_tick = world.calendar.ticks_per_day * world.calendar.days_per_week as u64;
let mut daily = Vec::new();
while world.tick < first_week_tick {
world.step();
if world.tick.is_multiple_of(world.calendar.ticks_per_day) {
daily.push((world.tick, world.people.len(), world.food));
}
}
assert!(
world.people.len() >= 10,
"village failed to replace its initial population: daily={daily:?}, people={:?}, events={:?}",
world
.people
.iter()
.map(|person| (person.id, person.hunger, person.action.as_str()))
.collect::<Vec<_>>(),
world.log,
);
assert!(
world.log.iter().all(|event| !event.contains("starved")),
"people starved before the first birth check: {:?}",
world.log
);
}
#[test]
fn legacy_snapshots_restore_the_village_as_adults() {
let world = World::new(42);
let mut snapshot: serde_json::Value =
serde_json::from_str(&world.snapshot_json().unwrap()).unwrap();
for person in snapshot["people"].as_array_mut().unwrap() {
let person = person.as_object_mut().unwrap();
for field in [
"age_ticks",
"stage",
"stage_elapsed_ticks",
"stage_duration_days",
"partner_id",
] {
person.remove(field);
}
}
snapshot.as_object_mut().unwrap().remove("partnerships");
let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap();
assert_eq!(restored.people.len(), 9);
assert_eq!(restored.people[0].stage, LifeStage::YoungAdult);
assert!(
restored
.people
.iter()
.skip(3)
.all(|person| person.stage == LifeStage::Adult)
);
}
#[test]
fn partnership_and_life_stage_are_in_persisted_actor_state() {
let mut world = World::new(42);
world.find_partners();
world.sync_entities();
let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap();
assert_eq!(restored.partnerships.len(), 4);
assert_eq!(restored.state_json(), world.state_json());
assert_eq!(
serde_json::from_str::<serde_json::Value>(&restored.state_json()).unwrap()["people"][0]
[8],
"young_adult"
);
}
#[test]
fn children_return_home_instead_of_staying_away_from_food() {
let mut world = World::new(42);
world.food = 100;
let home = world.home;
let position = (home.0.saturating_add(5), home.1.saturating_add(5));
world.spawn_person_at(position);
world.tick_person(world.people.len() - 1);
let child = world.people.last().unwrap();
assert!(cheb((child.x, child.y), home) < cheb(position, home));
assert_eq!(child.action, "growing up");
}
#[test]
fn a_person_eats_available_food_before_starving() {
let mut world = World::new(42);
world.food = 1;
world.people[0].hunger = 99.0;
world.tick_person(0);
assert!(!world.people[0].dead);
assert!(world.people[0].hunger < 100.0);
assert_eq!(world.food, 0);
}
#[test]
fn snapshot_restores_state_and_rng() {
let mut original = World::new(42);
for _ in 0..10 {
original.step();
}
original.paused = true;
original.speed = 4;
let snapshot = original.snapshot_json().unwrap();
let mut restored = World::from_snapshot(&snapshot, 42).unwrap();
assert_eq!(original.state_json(), restored.state_json());
original.step();
restored.step();
assert_eq!(original.state_json(), restored.state_json());
}
#[test]
fn reset_keeps_seed_and_settings_with_nine_people() {
let mut original = World::new(42);
original.apply("speed 4");
original.paused = true;
original.set_script(DEFAULT_SCRIPT).unwrap();
original.step();
let reset = original.restarted(original.seed());
assert_eq!(reset.seed(), 42);
assert_eq!(reset.speed, 4);
assert!(reset.paused);
assert_eq!(reset.people.len(), 9);
assert_eq!(reset.tick, DEFAULT_TICKS_PER_DAY / 4);
}
#[test]
fn regenerated_seed_differs_from_current_seed() {
let world = World::new(42);
assert_ne!(world.regenerated_seed(), world.seed());
}
#[test]
fn picard_actors_own_living_entity_state() {
let mut world = World::new(42);
let person_id = world.people[0].id;
world.apply(&format!("person {person_id} hunger 73"));
world.people[0].hunger = 1.0;
world.load_entities();
assert_eq!(world.people[0].hunger, 73.0);
assert_eq!(
world.entities.len(),
world.people.len() + world.rabbits.len() + world.wolves.len()
);
}
#[test]
fn calendar_and_life_stage_ranges_persist_in_snapshots() {
let mut world = World::new(42);
world.apply("calendar ticks_per_day 240");
world.apply("calendar days_per_week 5");
world.apply("stage toddler 3 9");
let snapshot = world.snapshot_json().unwrap();
let restored = World::from_snapshot(&snapshot, 42).unwrap();
assert_eq!(restored.calendar.ticks_per_day, 240);
assert_eq!(restored.calendar.days_per_week, 5);
assert_eq!(restored.calendar.toddler.min_days, 3);
assert_eq!(restored.calendar.toddler.max_days, 9);
assert_eq!(world.state_json(), restored.state_json());
}
#[test]
fn snapshots_without_calendar_use_default_settings() {
let world = World::new(42);
let mut snapshot: serde_json::Value =
serde_json::from_str(&world.snapshot_json().unwrap()).unwrap();
snapshot.as_object_mut().unwrap().remove("calendar");
let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap();
assert_eq!(restored.calendar.ticks_per_day, DEFAULT_TICKS_PER_DAY);
assert_eq!(restored.calendar.days_per_week, 7);
}
}