From 63c72f5df7aa9d5ee0c7be23446b74110cf1d0e7 Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Sun, 11 Oct 2026 21:23:50 +0200 Subject: [PATCH] refactor(sim): resolve clippy pedantic lints - Replace signed coordinate round-trips with checked offset helpers - Use From/try_from conversions and clamp helpers in the edit parser - Isolate exact RNG, noise and day-fraction float casts in documented helpers - Split long functions (validate, world creation, apply, state_json, tick_person) - Pass run_sim shared state by reference and tidy remaining style lints Simulation behaviour and RNG sequences are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main.rs | 20 +- src/world.rs | 1025 +++++++++++++++++++++++++++++--------------------- 2 files changed, 600 insertions(+), 445 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2cae2ac..d5a5f29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,10 +57,10 @@ fn run_sim( snapshot, }: SimStart, mut rx: mpsc::UnboundedReceiver, - terrain: Arc>, - script: Arc>, - state_tx: watch::Sender>, - snapshot_tx: watch::Sender>, + terrain: &Mutex, + script: &Mutex, + state_tx: &watch::Sender>, + snapshot_tx: &watch::Sender>, ) { let mut world = match snapshot { Some(snapshot) => World::from_snapshot_with_script(&snapshot, seed, sim_script) @@ -235,7 +235,7 @@ async fn client(mut socket: WebSocket, app: App) { Some(Ok(Message::Text(t))) if t.len() <= 256 => { let _ = app.tx.send(Msg::Edit(t.as_str().to_owned())); } - Some(Ok(Message::Close(_))) | Some(Err(_)) | None => return, + Some(Ok(Message::Close(_)) | Err(_)) | None => return, _ => {} }, } @@ -309,11 +309,11 @@ async fn main() { snapshot: saved_snapshot, }, rx, - terrain_buf, - script, - state_tx, - snapshot_tx, - ) + &terrain_buf, + &script, + &state_tx, + &snapshot_tx, + ); }); } diff --git a/src/world.rs b/src/world.rs index 5bb37de..db3fb05 100644 --- a/src/world.rs +++ b/src/world.rs @@ -168,6 +168,14 @@ pub struct SimulationConfig { impl SimulationConfig { fn validate(&self) -> Result<(), String> { + self.validate_limits()?; + self.validate_calendar()?; + self.validate_chances()?; + self.validate_world()?; + self.validate_amounts() + } + + fn validate_limits(&self) -> Result<(), String> { if !(8..=256).contains(&self.width) || !(8..=256).contains(&self.height) { return Err("world width and height must be between 8 and 256".into()); } @@ -204,7 +212,10 @@ impl SimulationConfig { if self.starting_tick_fraction == 0 { return Err("starting_tick_fraction must be at least 1".into()); } + Ok(()) + } + fn validate_calendar(&self) -> Result<(), String> { let calendar = &self.calendar; if calendar.ticks_per_day == 0 || calendar.ticks_per_day > 1_000_000 @@ -231,6 +242,10 @@ impl SimulationConfig { return Err("life-stage durations must be in the range 1..=1000000".into()); } } + Ok(()) + } + + fn validate_chances(&self) -> Result<(), String> { for (name, chance) in [ ( "person_hunt_success_chance", @@ -250,6 +265,10 @@ impl SimulationConfig { return Err(format!("{name} must be between 0 and 1")); } } + Ok(()) + } + + fn validate_world(&self) -> Result<(), String> { if self.animal_population_interval == 0 || self.entity_update_interval < 2 { return Err( "animal_population_interval must be positive and entity_update_interval at least 2" @@ -306,6 +325,10 @@ impl SimulationConfig { return Err(format!("{name} must be finite")); } } + Ok(()) + } + + fn validate_amounts(&self) -> Result<(), String> { if self.birth_food_cost > self.birth_food_threshold { return Err("birth_food_cost cannot exceed birth_food_threshold".into()); } @@ -524,13 +547,10 @@ impl SimScript { .with_context(&context) .with_diagnostics(&mut diagnostics) .build(); - let unit = match unit { - Ok(unit) => unit, - Err(_) => { - let mut buffer = Buffer::no_color(); - let _ = diagnostics.emit(&mut buffer, &sources); - return Err(String::from_utf8_lossy(buffer.as_slice()).into_owned()); - } + let Ok(unit) = unit else { + let mut buffer = Buffer::no_color(); + let _ = diagnostics.emit(&mut buffer, &sources); + return Err(String::from_utf8_lossy(buffer.as_slice()).into_owned()); }; let runtime = Arc::new(context.runtime().map_err(|e| e.to_string())?); let unit = Arc::new(unit); @@ -692,41 +712,92 @@ impl Rng { x ^= x << 25; x ^= x >> 27; self.0 = x; - x.wrapping_mul(0x2545F4914F6CDD1D) + x.wrapping_mul(0x2545_F491_4F6C_DD1D) } fn f(&mut self) -> f32 { - (self.next() >> 40) as f32 / (1u64 << 24) as f32 + unit_f32(self.next()) } fn chance(&mut self, p: f32) -> bool { self.f() < p } fn below(&mut self, n: usize) -> usize { - (self.next() % n as u64) as usize + let n = u64::try_from(n).expect("usize fits in u64"); + usize::try_from(self.next() % n).expect("value below a usize bound fits in usize") + } +} + +/// Maps the top 24 bits of `bits` to `[0, 1)`. +#[allow(clippy::cast_precision_loss)] +fn unit_f32(bits: u64) -> f32 { + // Values below 2^24 are exactly representable in f32. + (bits >> 40) as f32 / (1u64 << 24) as f32 +} + +/// Converts a map coordinate or dimension (at most 256) to `f32` exactly. +fn coord_f32(n: usize) -> f32 { + f32::from(u16::try_from(n).expect("map coordinates fit in u16")) +} + +/// Parses the integer argument at `i` of an edit command. +fn arg(t: &[&str], i: usize) -> Option { + t.get(i).and_then(|s| s.parse().ok()) +} + +/// Clamps `v` into `lo..=hi` and converts it; the bounds must fit in `T`. +fn clamp_to>(v: i64, lo: i64, hi: i64) -> T +where + T::Error: std::fmt::Debug, +{ + T::try_from(v.clamp(lo, hi)).expect("edit bounds fit the target type") +} + +/// Converts an edit value to `f32`, rounding values beyond 2^24 like `as` does. +#[allow(clippy::cast_precision_loss)] +fn small_f32(v: i64) -> f32 { + v as f32 +} + +/// Sign of the step from `from` towards `to` along one axis. +fn direction(from: usize, to: usize) -> isize { + match to.cmp(&from) { + std::cmp::Ordering::Greater => 1, + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, } } 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); + let mut h = i64::from(x) + .cast_unsigned() + .wrapping_mul(0x9E37_79B9_7F4A_7C15) + ^ i64::from(y) + .cast_unsigned() + .wrapping_mul(0xC2B2_AE3D_27D4_EB4F) + ^ seed.wrapping_mul(0x1656_67B1_9E37_79F9); h ^= h >> 33; - h = h.wrapping_mul(0xFF51AFD7ED558CCD); + h = h.wrapping_mul(0xFF51_AFD7_ED55_8CCD); h ^= h >> 33; - (h >> 40) as f32 / (1u64 << 24) as f32 + unit_f32(h) +} + +/// Lattice cell containing `v`. +#[allow(clippy::cast_possible_truncation)] +fn lattice(v: f32) -> i32 { + // Noise inputs are bounded map coordinates; `as` saturates for anything larger. + v.floor() as i32 } 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 + let smooth = |t: f32| t * t * (3.0 - 2.0 * t); + let (xi, yi) = (lattice(x), lattice(y)); + let (u, v) = (smooth(x - x.floor()), smooth(y - y.floor())); + let top_left = hash(xi, yi, seed); + let top_right = hash(xi + 1, yi, seed); + let bottom_left = hash(xi, yi + 1, seed); + let bottom_right = hash(xi + 1, yi + 1, seed); + let top = top_left + (top_right - top_left) * u; + let bottom = bottom_left + (bottom_right - bottom_left) * u; + top + (bottom - top) * v } fn fbm(x: f32, y: f32, seed: u64, octaves: usize, scale: f32) -> f32 { @@ -741,6 +812,8 @@ fn fbm(x: f32, y: f32, seed: u64, octaves: usize, scale: f32) -> f32 { } /// What an entity senses this tick; passed to the Rune behaviour scripts. +// A flat record of independent sensor flags keeps field access simple in `config.rn`. +#[allow(clippy::struct_excessive_bools)] #[derive(Any, Default, Clone)] struct Ctx { #[rune(get)] @@ -966,26 +1039,26 @@ impl World { Self::new_with_script(seed, default_script()) } - pub fn new_with_script(seed: u64, script: SimScript) -> World { - let config = script.config.clone(); + fn generate_terrain(seed: u64, config: &SimulationConfig) -> Vec { let (width, height) = (config.width, config.height); let mut tiles = vec![0u8; width * height]; for y in 0..height { for x in 0..width { - let dx = (x as f32 / width as f32 - 0.5) * 2.0; - let dy = (y as f32 / height as f32 - 0.5) * 2.0; + let (fx, fy) = (coord_f32(x), coord_f32(y)); + let dx = (fx / coord_f32(width) - 0.5) * 2.0; + let dy = (fy / coord_f32(height) - 0.5) * 2.0; let d = (dx * dx + dy * dy).min(config.terrain_edge_distance_cap); let h = fbm( - x as f32, - y as f32, + fx, + fy, seed, config.terrain_noise_octaves, config.terrain_noise_scale, ) + config.terrain_edge_height - config.terrain_edge_falloff * d; let m = fbm( - x as f32 + config.terrain_moisture_offset, - y as f32 + config.terrain_moisture_offset, + fx + config.terrain_moisture_offset, + fy + config.terrain_moisture_offset, seed.wrapping_add(config.terrain_moisture_seed_offset), config.terrain_noise_octaves, config.terrain_noise_scale, @@ -1009,6 +1082,70 @@ impl World { }; } } + tiles + } + + fn populate(&mut self) { + for i in 0..self.config.initial_population { + self.spawn_person(); + let stage = if i < self.config.initial_young_adults { + 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| u64::from(self.sample_stage_duration(past_stage))) + .sum(); + let person = self.people.last_mut().expect("just spawned person"); + person.stage = stage; + person.stage_duration_days = duration_days; + person.age_ticks = age_days * self.calendar.ticks_per_day; + } + for _ in 0..self.config.initial_rabbits { + if let Some(p) = self.random_land() { + self.rabbits.push(Animal { + actor_id: 0, + x: p.0, + y: p.1, + energy: self.config.initial_animal_energy, + dead: false, + }); + } + } + for _ in 0..self.config.initial_wolves { + if let Some(p) = self.random_land() + && cheb(p, self.home) > self.config.wolf_min_home_distance + { + self.wolves.push(Animal { + actor_id: 0, + x: p.0, + y: p.1, + energy: self.config.initial_animal_energy, + dead: false, + }); + } + } + } + + pub fn new_with_script(seed: u64, script: SimScript) -> World { + let config = script.config.clone(); + let (width, height) = (config.width, config.height); + let tiles = Self::generate_terrain(seed, &config); let mut w = World { seed, width, @@ -1029,7 +1166,7 @@ impl World { wood: 0, stone: 0, tools: 0, - rng: Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1), + rng: Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1), tick: (config.calendar.ticks_per_day / config.starting_tick_fraction).max(1), next_id: 1, log: vec![], @@ -1059,60 +1196,7 @@ impl World { } let (hx, hy) = w.home; w.res[hy * width + hx] = (0, 0); - for i in 0..w.config.initial_population { - w.spawn_person(); - let stage = if i < w.config.initial_young_adults { - 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..w.config.initial_rabbits { - if let Some(p) = w.random_land() { - w.rabbits.push(Animal { - actor_id: 0, - x: p.0, - y: p.1, - energy: w.config.initial_animal_energy, - dead: false, - }); - } - } - for _ in 0..w.config.initial_wolves { - if let Some(p) = w.random_land() - && cheb(p, w.home) > w.config.wolf_min_home_distance - { - w.wolves.push(Animal { - actor_id: 0, - x: p.0, - y: p.1, - energy: w.config.initial_animal_energy, - dead: false, - }); - } - } + w.populate(); w.sync_entities(); w } @@ -1246,7 +1330,7 @@ impl World { fn random_land(&mut self) -> Option<(usize, usize)> { for _ in 0..self.config.random_land_attempts { let (x, y) = (self.rng.below(self.width), self.rng.below(self.height)); - if self.walkable(x as i32, y as i32) { + if self.walkable((x, y)) { return Some((x, y)); } } @@ -1295,10 +1379,15 @@ impl World { }, }; let range = duration.max_days - duration.min_days + 1; - duration.min_days + self.rng.below(range as usize) as u32 + duration.min_days + + u32::try_from(self.rng.below(range as usize)).expect("sample is below a u32 range") } - fn say(&mut self, s: String) { + fn population_u32(&self) -> u32 { + u32::try_from(self.people.len()).unwrap_or(u32::MAX) + } + + fn say(&mut self, s: &str) { self.log.push(format!( "[day {}] {}", self.tick / self.calendar.ticks_per_day + 1, @@ -1309,42 +1398,60 @@ impl World { } } - pub fn daylight(&self) -> f32 { + /// Fraction of the current day that has elapsed, in `[0, 1)`. + #[allow(clippy::cast_precision_loss)] + fn day_fraction(&self) -> f32 { + // `ticks_per_day` is validated to at most 1,000,000, which f32 represents exactly. let ticks_per_day = self.calendar.ticks_per_day; - let phase = (self.tick % ticks_per_day) as f32 / ticks_per_day as f32; + (self.tick % ticks_per_day) as f32 / ticks_per_day as f32 + } + + pub fn daylight(&self) -> f32 { + let phase = self.day_fraction(); let v = -(phase * std::f32::consts::TAU).cos(); (self.config.daylight_base + v * self.config.daylight_amplitude).clamp(0.0, 1.0) } - fn walkable(&self, x: i32, y: i32) -> bool { - if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { - return false; - } - matches!( - self.tiles[y as usize * self.width + x as usize], - SAND | GRASS | FOREST | HILLS - ) + fn walkable(&self, (x, y): (usize, usize)) -> bool { + x < self.width + && y < self.height + && matches!( + self.tiles[y * self.width + x], + 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); - } + /// The in-bounds tile at `(x + dx, y + dy)`, if any. + fn offset(&self, (x, y): (usize, usize), dx: isize, dy: isize) -> Option<(usize, usize)> { + let x = x.checked_add_signed(dx).filter(|&x| x < self.width)?; + let y = y.checked_add_signed(dy).filter(|&y| y < self.height)?; + Some((x, y)) + } + + /// The walkable tile at `(x + dx, y + dy)`, excluding the origin itself. + fn walk_to(&self, from: (usize, usize), dx: isize, dy: isize) -> Option<(usize, usize)> { + if (dx, dy) == (0, 0) { + return None; } - self.wander(x, y) + self.offset(from, dx, dy).filter(|&to| self.walkable(to)) + } + + fn move_dir(&mut self, x: usize, y: usize, dx: isize, dy: isize) -> (usize, usize) { + [(dx, dy), (dx, 0), (0, dy)] + .into_iter() + .find_map(|(a, b)| self.walk_to((x, y), a, b)) + .unwrap_or_else(|| self.wander(x, y)) } fn wander(&mut self, x: usize, y: usize) -> (usize, usize) { - let radius = self.config.wander_radius as i32; - let diameter = self.config.wander_radius * 2 + 1; + let radius = self.config.wander_radius; + let diameter = radius * 2 + 1; + let signed_radius = isize::try_from(radius).expect("wander radius is validated"); for _ in 0..self.config.wander_attempts { - let (a, b) = ( - self.rng.below(diameter) as i32 - radius, - self.rng.below(diameter) as i32 - radius, - ); - 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); + let a = isize::try_from(self.rng.below(diameter)).expect("bounded by diameter"); + let b = isize::try_from(self.rng.below(diameter)).expect("bounded by diameter"); + if let Some(to) = self.walk_to((x, y), a - signed_radius, b - signed_radius) { + return to; } } (x, y) @@ -1378,22 +1485,17 @@ impl World { 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) { + let Some((x, y)) = self.walk_to(position, dx, dy) else { continue; - } - let neighbor = y as usize * self.width + x as usize; + }; + let neighbor = y * self.width + x; 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, - ))); + frontier.push(Reverse((cost + cheb((x, y), to), neighbor))); } } } @@ -1469,7 +1571,7 @@ impl World { 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; + u64::from(self.people[i].stage_duration_days) * self.calendar.ticks_per_day; if self.people[i].stage_elapsed_ticks < duration_ticks { continue; } @@ -1494,7 +1596,7 @@ impl World { } } for notice in notices { - self.say(notice); + self.say(¬ice); } } @@ -1529,13 +1631,13 @@ impl World { let p = self.rng.f(); match self.tiles[i] { FOREST if p < self.config.tree_regrow_chance => { - self.res[i] = (TREE, self.config.tree_regrow_amount) + self.res[i] = (TREE, self.config.tree_regrow_amount); } GRASS if p < self.config.berry_regrow_chance => { - self.res[i] = (BERRY, self.config.berry_regrow_amount) + self.res[i] = (BERRY, self.config.berry_regrow_amount); } HILLS if p < self.config.stone_regrow_chance => { - self.res[i] = (STONE, self.config.stone_regrow_amount) + self.res[i] = (STONE, self.config.stone_regrow_amount); } _ => {} } @@ -1547,7 +1649,7 @@ impl World { 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; + let ticks_per_week = ticks_per_day * u64::from(self.calendar.days_per_week); if self.tick.is_multiple_of(ticks_per_week) { self.have_children(ticks_per_week); } @@ -1583,7 +1685,7 @@ impl World { } fn find_partners(&mut self) { - let ticks_per_week = self.calendar.ticks_per_day * self.calendar.days_per_week as u64; + let ticks_per_week = self.calendar.ticks_per_day * u64::from(self.calendar.days_per_week); for first in 0..self.people.len() { if self.people[first].partner_id.is_some() || !self.people[first].stage.can_partner() @@ -1622,7 +1724,7 @@ impl World { actor_id: 0, }); self.next_partnership_id += 1; - self.say(format!( + self.say(&format!( "People #{first_id} and #{second_id} became partners" )); } @@ -1684,7 +1786,7 @@ impl World { { partnership.last_birth_tick = self.tick; } - self.say(format!( + self.say(&format!( "People #{first_id} and #{second_id} welcomed a toddler" )); } @@ -1704,15 +1806,8 @@ impl World { } } - fn tick_person(&mut self, i: usize) { - let mut p = self.people[i].clone(); - if p.dead { - return; - } - let night = self.daylight() < self.config.daylight_night_threshold; - let pos = (p.x, p.y); - let home = self.home; - let at_home = cheb(pos, home) <= self.config.home_radius; + /// Updates hunger, deposits carried goods at home and eats when hungry. + fn feed_person(&mut self, p: &mut Person, night: bool, at_home: bool) { p.hunger += if night && at_home { self.config.hunger_night_at_home } else { @@ -1739,9 +1834,46 @@ impl World { p.food -= self.config.food_per_meal; p.hunger = (p.hunger - self.config.hunger_meal_reduction).max(0.0); } + } + + /// Harvests `kind` at `target` when standing on it, otherwise walks there. + fn gather(&mut self, p: &mut Person, kind: u8, target: (usize, usize)) { + let pos = (p.x, p.y); + let idx = pos.1 * self.width + pos.0; + if pos != target || self.res[idx].0 != kind { + (p.x, p.y) = self.move_toward(pos, target); + return; + } + let yield_n: u16 = if self.tools >= self.population_u32() { + self.config.person_tooled_gather_yield + } else { + self.config.person_gather_yield + }; + 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 += u32::from(take), + TREE => p.wood += u32::from(take), + _ => p.stone += u32::from(take), + } + } + + fn tick_person(&mut self, i: usize) { + let mut p = self.people[i].clone(); + if p.dead { + return; + } + let night = self.daylight() < self.config.daylight_night_threshold; + let pos = (p.x, p.y); + let home = self.home; + let at_home = cheb(pos, home) <= self.config.home_radius; + self.feed_person(&mut p, night, at_home); if p.hunger >= self.config.hunger_starvation_threshold { p.dead = true; - self.say(format!("Person #{} starved", p.id)); + self.say(&format!("Person #{} starved", p.id)); self.people[i] = p; return; } @@ -1764,29 +1896,23 @@ impl World { let tree = self.nearest_res(pos, TREE, self.config.resource_search_radius); let stone = self.nearest_res(pos, STONE, self.config.resource_search_radius); let rabbit = Self::nearest_animal(&self.rabbits, pos, self.config.resource_search_radius); - let c = Ctx { + let ctx = 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, + hunger: f64::from(p.hunger), + carry: i64::from(p.carry()), + carried_food: i64::from(p.food), + stock_food: i64::from(self.food), + stock_wood: i64::from(self.wood), + stock_stone: i64::from(self.stone), 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, + roll: f64::from(self.rng.f()), ..Default::default() }; - let choice = self.decide("person", c); - - let yield_n: u16 = if self.tools >= self.people.len() as u32 { - self.config.person_tooled_gather_yield - } else { - self.config.person_gather_yield - }; + let choice = self.decide("person", ctx); let gather = match choice.as_str() { "forage" => Some((BERRY, berry, "foraging")), "chop" => Some((TREE, tree, "chopping wood")), @@ -1806,35 +1932,21 @@ impl World { (p.x, p.y) = self.move_toward(pos, home); } } - ("hunt", _, Some(r)) => { + ("hunt", _, Some(rabbit)) => { p.action = "hunting".to_string(); - let t = (self.rabbits[r].x, self.rabbits[r].y); - if cheb(pos, t) <= self.config.attack_range { + let target = (self.rabbits[rabbit].x, self.rabbits[rabbit].y); + if cheb(pos, target) <= self.config.attack_range { if self.rng.chance(self.config.person_hunt_success_chance) { - self.rabbits[r].dead = true; + self.rabbits[rabbit].dead = true; p.food += self.config.person_hunt_food_yield; } } else { - (p.x, p.y) = self.move_toward(pos, t); + (p.x, p.y) = self.move_toward(pos, target); } } - (_, Some((kind, Some(t), label)), _) => { + (_, Some((kind, Some(target), label)), _) => { p.action = label.to_string(); - let idx = pos.1 * self.width + 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); - } + self.gather(&mut p, kind, target); } _ => { p.action = "wandering".to_string(); @@ -1847,7 +1959,7 @@ impl World { fn craft_and_build(&mut self) { if self.wood >= self.config.tool_wood_cost && self.stone >= self.config.tool_stone_cost - && self.tools < self.people.len() as u32 + && self.tools < self.population_u32() { self.wood -= self.config.tool_wood_cost; self.stone -= self.config.tool_stone_cost; @@ -1860,18 +1972,20 @@ impl World { let (hx, hy) = self.home; let radius = self.config.house_placement_radius; let diameter = radius * 2 + 1; - let x = (hx as i32 + self.rng.below(diameter) as i32 - radius as i32) - .clamp(0, self.width as i32 - 1); - let y = (hy as i32 + self.rng.below(diameter) as i32 - radius as i32) - .clamp(0, self.height as i32 - 1); - if self.walkable(x, y) - && self.res[y as usize * self.width + x as usize].0 == 0 - && !self.houses.contains(&(x as usize, y as usize)) + let x = (hx + self.rng.below(diameter)) + .saturating_sub(radius) + .min(self.width - 1); + let y = (hy + self.rng.below(diameter)) + .saturating_sub(radius) + .min(self.height - 1); + if self.walkable((x, y)) + && self.res[y * self.width + x].0 == 0 + && !self.houses.contains(&(x, y)) { self.wood -= self.config.house_wood_cost; self.stone -= self.config.house_stone_cost; - self.houses.push((x as usize, y as usize)); - self.say("A new house was built".into()); + self.houses.push((x, y)); + self.say("A new house was built"); } } } @@ -1887,13 +2001,13 @@ impl World { let wolf = Self::nearest_animal(&self.wolves, pos, self.config.rabbit_wolf_detection_range); let c = Ctx { wolf_near: wolf.is_some(), - roll: self.rng.f() as f64, + roll: f64::from(self.rng.f()), ..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(); + let dx = direction(self.wolves[w].x, a.x); + let dy = direction(self.wolves[w].y, a.y); (a.x, a.y) = self.move_dir(a.x, a.y, dx, dy); } ("wander", _) => (a.x, a.y) = self.wander(a.x, a.y), @@ -1919,46 +2033,45 @@ impl World { } fn tick_wolf(&mut self, i: usize) { - let mut a = self.wolves[i].clone(); - a.energy -= self.config.wolf_energy_drain; - let pos = (a.x, a.y); + let mut wolf = self.wolves[i].clone(); + wolf.energy -= self.config.wolf_energy_drain; + let pos = (wolf.x, wolf.y); let prey = Self::nearest_animal(&self.rabbits, pos, self.config.wolf_prey_range); - let c = Ctx { + let ctx = 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) <= self.config.attack_range { - if self.rng.chance(self.config.wolf_hunt_success_chance) { - self.rabbits[r].dead = true; - a.energy = (a.energy + self.config.wolf_hunt_energy_gain) - .min(self.config.wolf_energy_max); - } - } else { - (a.x, a.y) = self.move_toward(pos, t); + if let ("hunt", Some(rabbit)) = (self.decide("wolf", ctx).as_str(), prey) { + let target = (self.rabbits[rabbit].x, self.rabbits[rabbit].y); + if cheb(pos, target) <= self.config.attack_range { + if self.rng.chance(self.config.wolf_hunt_success_chance) { + self.rabbits[rabbit].dead = true; + wolf.energy = (wolf.energy + self.config.wolf_hunt_energy_gain) + .min(self.config.wolf_energy_max); } + } else { + (wolf.x, wolf.y) = self.move_toward(pos, target); } - _ => (a.x, a.y) = self.wander(a.x, a.y), + } else { + (wolf.x, wolf.y) = self.wander(wolf.x, wolf.y); } - if a.energy > self.config.wolf_breed_energy + if wolf.energy > self.config.wolf_breed_energy && self.wolves.len() < self.config.wolf_max_population && self.rng.chance(self.config.wolf_breed_chance) { - a.energy -= self.config.wolf_breed_energy_cost; + wolf.energy -= self.config.wolf_breed_energy_cost; self.wolves.push(Animal { actor_id: 0, - x: a.x, - y: a.y, + x: wolf.x, + y: wolf.y, energy: self.config.wolf_newborn_energy, dead: false, }); } - if a.energy <= self.config.animal_energy_death_threshold { - a.dead = true; + if wolf.energy <= self.config.animal_energy_death_threshold { + wolf.dead = true; } - self.wolves[i] = a; + self.wolves[i] = wolf; } /// Replaces the running script. Behaviours and tuning constants apply @@ -2141,7 +2254,7 @@ impl World { }; let age_days: u64 = prior_stages .iter() - .map(|&past_stage| self.sample_stage_duration(past_stage) as u64) + .map(|&past_stage| u64::from(self.sample_stage_duration(past_stage))) .sum(); let person = &mut self.people[index]; person.stage = stage; @@ -2156,7 +2269,7 @@ impl World { if n.len() != 4 { return None; } - let c = |v: i64, max: usize| v.clamp(0, max as i64 - 1) as usize; + let c = |v: i64, max: usize| usize::try_from(v.max(0)).map_or(max - 1, |v| v.min(max - 1)); let (x0, x1) = (c(n[0].min(n[2]), self.width), c(n[0].max(n[2]), self.width)); let (y0, y1) = ( c(n[1].min(n[3]), self.height), @@ -2169,188 +2282,214 @@ impl World { 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(property)) = (t.get(1), t.get(2)) { - if *property == "fertile" { - if let Some(value) = num(3) - && 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 - && let Some(range) = self.calendar.duration_mut(stage) - { - range.min_days = min_days; - range.max_days = max_days; - } - } - } - } + Some("pause") => self.paused = arg(&t, 1) != Some(0), + Some("calendar") => self.edit_calendar(&t), + Some("stage") => self.edit_stage(&t), Some("speed") => { - if let Some(n) = num(1) { - self.speed = n.clamp(1, self.config.max_simulation_speed as i64) as u32; - } - } - Some("stock") => { - if let (Some(name), Some(v)) = (t.get(1), num(2)) { - let v = v.clamp(0, self.config.max_stock_edit as i64) 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)) - && let Some(p) = self.people.iter_mut().find(|p| p.id as i64 == id) - { - let u = v.clamp(0, self.config.max_person_inventory_edit as i64) as u32; - match *field { - "hunger" => p.hunger = v.clamp(0, self.config.max_hunger_edit) 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, self.config.max_animal_energy_edit as i64) 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; - } - } + if let Some(n) = arg(&t, 1) { + self.speed = clamp_to(n, 1, i64::from(self.config.max_simulation_speed)); } } + Some("stock") => self.edit_stock(&t), + Some("person") => self.edit_person(&t), + Some("energy") => self.edit_energy(&t), Some("terrain") => { - if let (Some((x0, y0, x1, y1)), Some(kind)) = (self.rect(&t, 1), num(5)) - && (0..=6).contains(&kind) + if let (Some((x0, y0, x1, y1)), Some(kind)) = (self.rect(&t, 1), arg(&t, 5)) + && let Ok(kind) = u8::try_from(kind) + && kind <= 6 { - 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)) - && (0..=3).contains(&kind) - { - let amt = amt.clamp(1, self.config.max_resource_edit as i64) as u16; - for y in y0..=y1 { - for x in x0..=x1 { - let i = y * self.width + 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 = self.config.manual_spawn_limit; - 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: self.config.initial_animal_energy, - 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() < self.config.max_houses - && self.walkable(x as i32, y as i32) - && self.res[y * self.width + 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 * self.width + x] = (0, 0); - } - } - } + self.paint_terrain(x0, y0, x1, y1, kind); } } + Some("res") => self.edit_resources(&t), + Some("spawn") => self.edit_spawn(&t), + Some("house") => self.edit_houses(&t), + Some("remove") => self.edit_remove(&t), _ => {} } self.prune_partnerships(); self.sync_entities(); } + fn edit_calendar(&mut self, t: &[&str]) { + if let (Some(setting), Some(value)) = (t.get(1), arg(t, 2)) { + let days: u32 = clamp_to(value, 1, 100); + match *setting { + "ticks_per_day" => self.calendar.ticks_per_day = clamp_to(value, 1, 1_000_000), + "days_per_week" => self.calendar.days_per_week = days, + "weeks_per_month" => self.calendar.weeks_per_month = days, + "months_per_year" => self.calendar.months_per_year = days, + _ => {} + } + } + } + + fn edit_stage(&mut self, t: &[&str]) { + let (Some(stage), Some(property)) = (t.get(1), t.get(2)) else { + return; + }; + if *property == "fertile" { + if let Some(value) = arg(t, 3) + && let Some(range) = self.calendar.duration_mut(stage) + { + range.fertile = value != 0; + } + } else if let (Some(min_days), Some(max_days)) = (arg(t, 2), arg(t, 3)) { + let min_days: u32 = clamp_to(min_days, 1, 1_000_000); + let max_days: u32 = clamp_to(max_days, 1, 1_000_000); + if min_days <= max_days + && let Some(range) = self.calendar.duration_mut(stage) + { + range.min_days = min_days; + range.max_days = max_days; + } + } + } + + fn edit_stock(&mut self, t: &[&str]) { + if let (Some(name), Some(v)) = (t.get(1), arg(t, 2)) { + let v = clamp_to(v, 0, i64::from(self.config.max_stock_edit)); + match *name { + "food" => self.food = v, + "wood" => self.wood = v, + "stone" => self.stone = v, + "tools" => self.tools = v, + _ => {} + } + } + } + + fn edit_person(&mut self, t: &[&str]) { + let max_inventory = i64::from(self.config.max_person_inventory_edit); + let max_hunger = self.config.max_hunger_edit; + if let (Some(id), Some(field), Some(v)) = (arg(t, 1), t.get(2), arg(t, 3)) + && let Some(p) = self.people.iter_mut().find(|p| i64::from(p.id) == id) + { + let u = clamp_to(v, 0, max_inventory); + match *field { + "hunger" => p.hunger = small_f32(v.clamp(0, max_hunger)), + "food" => p.food = u, + "wood" => p.wood = u, + "stone" => p.stone = u, + _ => {} + } + } + } + + fn edit_energy(&mut self, t: &[&str]) { + if let (Some((x0, y0, x1, y1)), Some(v)) = (self.rect(t, 1), arg(t, 5)) { + let max = self.config.max_animal_energy_edit.trunc().max(1.0); + let v = small_f32(v.clamp(1, 1_000_000)).min(max); + 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; + } + } + } + } + + fn edit_resources(&mut self, t: &[&str]) { + let (Some((x0, y0, x1, y1)), Some(kind), Some(amt)) = + (self.rect(t, 1), arg(t, 5), arg(t, 6)) + else { + return; + }; + let Ok(kind) = u8::try_from(kind) else { + return; + }; + if kind > 3 { + return; + } + let amt = clamp_to(amt, 1, i64::from(self.config.max_resource_edit)); + for y in y0..=y1 { + for x in x0..=x1 { + let i = y * self.width + x; + let free = + self.walkable((x, y)) && (x, y) != self.home && !self.houses.contains(&(x, y)); + if kind == 0 { + self.res[i] = (0, 0); + } else if free { + self.res[i] = (kind, amt); + } + } + } + } + + fn edit_spawn(&mut self, t: &[&str]) { + let (Some(what), Some((x0, y0, x1, y1))) = (t.get(1), self.rect(t, 2)) else { + return; + }; + let mut left = self.config.manual_spawn_limit; + for y in y0..=y1 { + for x in x0..=x1 { + if left == 0 || !self.walkable((x, y)) { + continue; + } + left -= 1; + let a = Animal { + actor_id: 0, + x, + y, + energy: self.config.initial_animal_energy, + dead: false, + }; + match *what { + "person" => self.spawn_person_at((x, y)), + "rabbit" => self.rabbits.push(a), + "wolf" => self.wolves.push(a), + _ => left += 1, + } + } + } + } + + fn edit_houses(&mut self, t: &[&str]) { + let Some((x0, y0, x1, y1)) = self.rect(t, 1) else { + return; + }; + for y in y0..=y1 { + for x in x0..=x1 { + if self.houses.len() < self.config.max_houses + && self.walkable((x, y)) + && self.res[y * self.width + x].0 == 0 + && (x, y) != self.home + && !self.houses.contains(&(x, y)) + { + self.houses.push((x, y)); + } + } + } + } + + fn edit_remove(&mut self, t: &[&str]) { + let (Some((x0, y0, x1, y1)), Some(what)) = (self.rect(t, 1), t.get(5)) else { + return; + }; + 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 * self.width + x] = (0, 0); + } + } + } + } + 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 { @@ -2388,50 +2527,9 @@ impl World { ) } - 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 actor_stats = self.actor_system.stats(); + fn write_calendar_json(&self, s: &mut String) { let _ = write!( - s, - "{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"limits\":{{\"speed\":{},\"stock\":{},\"person_inventory\":{},\"hunger\":{},\"animal_energy\":{},\"resource\":{},\"spawn\":{}}},\"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.config.max_simulation_speed, - self.config.max_stock_edit, - self.config.max_person_inventory_edit, - self.config.max_hunger_edit, - self.config.max_animal_energy_edit, - self.config.max_resource_edit, - self.config.manual_spawn_limit, - self.food, - self.wood, - self.stone, - self.tools, - self.home.0, - self.home.1 - ); - let _ = write!( - s, + *s, "\"calendar\":{{\"ticks_per_day\":{},\"days_per_week\":{},\"weeks_per_month\":{},\"months_per_year\":{},", self.calendar.ticks_per_day, self.calendar.days_per_week, @@ -2454,12 +2552,15 @@ impl World { s.push(','); } let _ = write!( - s, + *s, "\"{name}\":{{\"min_days\":{},\"max_days\":{},\"fertile\":{}}}", range.min_days, range.max_days, range.fertile ); } s.push_str("},"); + } + + fn write_entities_json(&self, s: &mut String) { s.push_str("\"houses\":["); let j: Vec = self .houses @@ -2512,12 +2613,65 @@ impl World { s.push(','); } first = false; - let _ = write!(s, "[{},{},{},{}]", i % self.width, i / self.width, r.0, r.1); + let _ = write!( + *s, + "[{},{},{},{}]", + i % self.width, + i / self.width, + r.0, + r.1 + ); } } s.push_str("],\"log\":["); - let j: Vec = self.log.iter().map(|l| format!("\"{}\"", l)).collect(); + let j: Vec = self.log.iter().map(|l| format!("\"{l}\"")).collect(); s.push_str(&j.join(",")); + } + + pub fn state_json(&self) -> String { + let mut s = String::new(); + let days_per_month = + u64::from(self.calendar.days_per_week) * u64::from(self.calendar.weeks_per_month); + let days_per_year = days_per_month * u64::from(self.calendar.months_per_year); + let day = self.tick / self.calendar.ticks_per_day; + let year = day / days_per_year + 1; + let month = (day / days_per_month) % u64::from(self.calendar.months_per_year) + 1; + let week = (day / u64::from(self.calendar.days_per_week)) + % u64::from(self.calendar.weeks_per_month) + + 1; + let date_day = day % u64::from(self.calendar.days_per_week) + 1; + let actor_stats = self.actor_system.stats(); + let _ = write!( + s, + "{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"limits\":{{\"speed\":{},\"stock\":{},\"person_inventory\":{},\"hunger\":{},\"animal_energy\":{},\"resource\":{},\"spawn\":{}}},\"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.day_fraction() * 24.0, + self.daylight(), + self.config.max_simulation_speed, + self.config.max_stock_edit, + self.config.max_person_inventory_edit, + self.config.max_hunger_edit, + self.config.max_animal_energy_edit, + self.config.max_resource_edit, + self.config.manual_spawn_limit, + self.food, + self.wood, + self.stone, + self.tools, + self.home.0, + self.home.1 + ); + self.write_calendar_json(&mut s); + self.write_entities_json(&mut s); let _ = write!( s, "],\"actor_stats\":{{\"actors_created\":{},\"actors_terminated\":{},\"actors_restarted\":{},\"messages_sent\":{},\"messages_failed\":{},\"system_messages_sent\":{},\"live_actors\":{}}}}}", @@ -2665,13 +2819,14 @@ mod tests { } assert_eq!(position, world.home); - assert!(world.walkable(start.0 as i32, start.1 as i32)); + assert!(world.walkable(start)); } #[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 first_week_tick = + world.calendar.ticks_per_day * u64::from(world.calendar.days_per_week); let mut daily = Vec::new(); while world.tick < first_week_tick { @@ -2826,7 +2981,7 @@ mod tests { world.people[0].hunger = 1.0; world.load_entities(); - assert_eq!(world.people[0].hunger, 73.0); + assert!((world.people[0].hunger - 73.0).abs() < f32::EPSILON); assert_eq!( world.entities.len(), world.people.len() + world.rabbits.len() + world.wolves.len()