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::collections::{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, } #[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, } } } 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, }, child: StageDuration { min_days: 4, max_days: 8, }, teen: StageDuration { min_days: 4, max_days: 8, }, young_adult: StageDuration { min_days: 6, max_days: 12, }, adult: StageDuration { min_days: 20, max_days: 40, }, older_adult: StageDuration { min_days: 10, max_days: 20, }, elderly: StageDuration { min_days: 4, max_days: 10, }, } } } impl CalendarSettings { 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, } } } 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; 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 { let mut module = Module::new(); module.ty::().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::(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, } 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), } #[derive(Clone, Debug)] struct EntityState { id: u64, kind: EntityKind, } #[derive(Debug)] enum EntityMessage { Read(SyncSender), Write(EntityState, SyncSender<()>), } struct EntityActor { state: EntityState, } impl ActorFactoryArgs for EntityActor { fn create_args(state: EntityState) -> Self { Self { state } } } impl Actor for EntityActor { type Msg = EntityMessage; fn recv(&mut self, _ctx: &ActorContext, 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, res: Vec<(u8, u16)>, people: Vec, rabbits: Vec, wolves: Vec, actor_system: ActorSystem, entities: HashMap>, 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, 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, tiles: Vec, res: Vec<(u8, u16)>, people: Vec, rabbits: Vec, wolves: Vec, houses: Vec<(usize, usize)>, home: (usize, usize), food: u32, wood: u32, stone: u32, tools: u32, rng: u64, tick: u64, next_id: u32, log: Vec, script: String, paused: bool, speed: u32, terrain_rev: u32, #[serde(default)] calendar: CalendarSettings, } 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![], 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: 4, 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 _ in 0..9 { w.spawn_person(); } 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>) -> Vec { 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(); 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), } } } 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; } } 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), })); 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::(&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) { let (x, y) = self.home; 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, }); self.next_id += 1; } 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) { let dx = (to.0 as i32 - from.0 as i32).signum(); let dy = (to.1 as i32 - from.1 as i32).signum(); self.move_dir(from.0, from.1, dx, dy) } 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 { 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(); 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.population(); self.sync_entities(); } 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) { if self.tick % 400 == 0 && self.food >= 10 && self.people.len() < 8 + self.houses.len() * 3 { self.food -= 6; self.spawn_person(); self.say("A child was born".into()); } 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 decide(&mut self, func: &str, c: Ctx) -> String { match self.vm.call([func], (c,)) { Ok(v) => rune::from_value::(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(); 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.04 } else { 0.1 }; if p.hunger >= 100.0 { p.dead = true; self.say(format!("Person #{} starved", p.id)); self.people[i] = p; return; } 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); } 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::(); if seed == self.seed { seed = seed.wrapping_add(1); } seed } pub fn seed(&self) -> u64 { self.seed } pub fn snapshot_json(&self) -> Result { 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(), }) } pub fn script(&self) -> &str { &self.script } pub fn from_snapshot(snapshot: &str, fallback_seed: u64) -> Result { let saved: WorldSnapshot = serde_json::from_str(snapshot).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, 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(); world.sync_entities(); Ok(world) } fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> { let n: Vec = (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::().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(min_days), Some(max_days)) = (t.get(1), 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.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\":{}}}", range.min_days, range.max_days ); } s.push_str("},"); s.push_str("\"houses\":["); let j: Vec = self .houses .iter() .map(|h| format!("[{},{}]", h.0, h.1)) .collect(); s.push_str(&j.join(",")); s.push_str("],\"people\":["); let j: Vec = self .people .iter() .map(|p| { format!( "[{},{},{},\"{}\",{:.0},{},{},{}]", p.id, p.x, p.y, p.action, p.hunger, p.food, p.wood, p.stone ) }) .collect(); s.push_str(&j.join(",")); s.push_str("],\"rabbits\":["); let j: Vec = 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 = 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 = 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, World}; #[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); } }