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);