From 52277ae7790392465c5a13e53c48435c68418748 Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Sun, 11 Oct 2026 18:13:10 +0200 Subject: [PATCH] fix(sim): improve population replacement rate Raise starter settlement capacity above its initial population and allow eligible partnerships one birth opportunity per week instead of a 20% monthly roll. Preserve food and housing constraints and cover both initial growth and per-week births in tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- src/world.rs | 54 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 27b7f70..94a9c29 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Living simulation entities (people, rabbits, and wolves) run as Picard actors. T The **Calendar and life-stage durations** panel configures ticks per day and the number of days, weeks, and months in larger calendar units. Each human life stage has an inclusive minimum/maximum duration in in-game days; the world samples within that range when a person enters the stage. These settings are stored with the world snapshot. -New people, including babies, start as toddlers and age through child, teen, young adult, adult, older adult, elderly, and death. The starting village is seeded with young and mature adults so the simulation can work and form the first partnerships immediately. Unpartnered people from young adulthood onward can form a single partnership with a nearby eligible person. Each partnership has up to one birth attempt per calendar month, with a 20% chance of having a toddler when both partners are in stages marked fertile, the stockpile has enough food, and housing capacity permits it. Young adult and adult stages are fertile by default; fertility can be changed per stage in the calendar panel. Partnerships are Picard actors as well as people and animals. +New people, including babies, start as toddlers and age through child, teen, young adult, adult, older adult, elderly, and death. The starting village is seeded with young and mature adults so the simulation can work and form the first partnerships immediately. Unpartnered people from young adulthood onward can form a single partnership with a nearby eligible person. Each partnership can have one toddler per calendar week when both partners are in stages marked fertile, the stockpile has enough food, and housing capacity permits it. The starting village has capacity for 12 people, with each additional house increasing capacity by three. Young adult and adult stages are fertile by default; fertility can be changed per stage in the calendar panel. Partnerships are Picard actors as well as people and animals. The UI is built automatically as part of `cargo run` and embedded in the server binary. There is no separate manual UI build step. diff --git a/src/world.rs b/src/world.rs index adc504e..eb8eb59 100644 --- a/src/world.rs +++ b/src/world.rs @@ -205,6 +205,7 @@ const MOUNTAIN: u8 = 6; const TREE: u8 = 1; const BERRY: u8 = 2; const STONE: u8 = 3; +const BASE_POPULATION_CAPACITY: usize = 12; pub struct Rng(u64); @@ -999,11 +1000,9 @@ impl World { if self.tick.is_multiple_of(ticks_per_day) { self.find_partners(); } - let ticks_per_month = ticks_per_day - * self.calendar.days_per_week as u64 - * self.calendar.weeks_per_month as u64; - if self.tick.is_multiple_of(ticks_per_month) { - self.have_children(ticks_per_month); + 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 { @@ -1078,8 +1077,8 @@ impl World { } } - fn have_children(&mut self, ticks_per_month: u64) { - if self.food < 10 || self.people.len() >= 8 + self.houses.len() * 3 { + fn have_children(&mut self, birth_interval_ticks: u64) { + if self.food < 10 || self.people.len() >= self.population_capacity() { return; } let partnerships: Vec<_> = self @@ -1095,10 +1094,10 @@ impl World { }) .collect(); for (partnership_id, first_id, second_id, last_birth_tick) in partnerships { - if self.food < 10 || self.people.len() >= 8 + self.houses.len() * 3 { + if self.food < 10 || self.people.len() >= self.population_capacity() { break; } - if self.tick.saturating_sub(last_birth_tick) < ticks_per_month { + 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 { @@ -1117,7 +1116,7 @@ impl World { .calendar .duration(second_stage.key()) .is_some_and(|duration| duration.fertile); - if !can_have_children || !self.rng.chance(0.2) { + if !can_have_children { continue; } @@ -1136,6 +1135,10 @@ impl World { } } + 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::(v).unwrap_or_else(|_| "wander".into()), @@ -1946,9 +1949,7 @@ mod tests { let mut world = World::new(42); world.calendar.ticks_per_day = 1; world.calendar.days_per_week = 1; - world.calendar.weeks_per_month = 1; world.food = 1000; - world.houses.push((0, 0)); world.find_partners(); assert_eq!(world.partnerships.len(), 4); assert_eq!( @@ -1971,13 +1972,12 @@ mod tests { assert_eq!(world.partnerships.len(), 4); let initial_population = world.people.len(); - for _ in 0..100 { + while world.people.len() < world.population_capacity() { world.tick += 1; world.have_children(1); - if world.people.len() > initial_population { - break; - } } + assert_eq!(world.people.len(), world.population_capacity()); + assert_eq!(world.people.len() - initial_population, 3); let toddler = world .people .iter() @@ -1987,6 +1987,28 @@ mod tests { 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 legacy_snapshots_restore_the_village_as_adults() { let world = World::new(42);