diff --git a/src/world.rs b/src/world.rs index d433621..407fc9a 100644 --- a/src/world.rs +++ b/src/world.rs @@ -2815,6 +2815,33 @@ mod tests { SimScript, W, World, cheb, default_config, }; + /// Replaces the value of `pub const {name}` in `source`, whatever its current value is. + fn with_constant(source: &str, name: &str, value: &str) -> String { + let prefix = format!("pub const {name} = "); + assert!( + source.lines().any(|line| line.starts_with(&prefix)), + "config.rn has no constant {name}" + ); + source + .lines() + .map(|line| { + if line.starts_with(&prefix) { + format!("{prefix}{value};") + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + } + + fn is_fertile(world: &World, stage: LifeStage) -> bool { + world + .calendar + .duration(stage.key()) + .is_some_and(|duration| duration.fertile) + } + fn simulation_state(world: &World) -> serde_json::Value { let mut state: serde_json::Value = serde_json::from_str(&world.state_json()).unwrap(); state.as_object_mut().unwrap().remove("actor_stats"); @@ -2863,15 +2890,22 @@ mod tests { world.calendar.ticks_per_day = 1; world.calendar.days_per_week = 1; world.food = 1000; + let eligible = world + .people + .iter() + .filter(|person| person.stage.can_partner()) + .count(); world.find_partners(); - assert_eq!(world.partnerships.len(), 4); + let partnerships = eligible / 2; + assert!(partnerships > 0); + assert_eq!(world.partnerships.len(), partnerships); assert_eq!( world .people .iter() .filter(|person| person.partner_id.is_some()) .count(), - 8 + partnerships * 2 ); world.people[0].x = 0; world.people[0].y = 0; @@ -2879,18 +2913,20 @@ mod tests { world.sync_entities(); assert_eq!( world.entities.len(), - world.people.len() + world.rabbits.len() + world.wolves.len() + 4 + world.people.len() + world.rabbits.len() + world.wolves.len() + partnerships ); world.load_entities(); - assert_eq!(world.partnerships.len(), 4); + assert_eq!(world.partnerships.len(), partnerships); let initial_population = world.people.len(); + let births = 3; + world.config.population_capacity = initial_population + births; 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); + assert_eq!(world.people.len() - initial_population, births); let toddler = world .people .iter() @@ -2904,21 +2940,39 @@ mod tests { 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(); + let fertile_partnerships = world + .partnerships + .iter() + .filter(|partnership| { + [partnership.first_person_id, partnership.second_person_id] + .iter() + .all(|id| { + let person = world.people.iter().find(|person| person.id == *id); + person.is_some_and(|person| is_fertile(&world, person.stage)) + }) + }) + .count(); + assert!(fertile_partnerships > 0); + let initial_population = world.people.len(); + world.config.population_capacity = initial_population + world.partnerships.len(); + world.food = world.config.birth_food_threshold + + world.config.birth_food_cost * u32::try_from(fertile_partnerships).unwrap(); world.tick += 7; world.have_children(7); - assert_eq!(world.people.len(), 13); + assert_eq!( + world.people.len(), + initial_population + fertile_partnerships + ); assert_eq!( world .people .iter() .filter(|person| person.stage == LifeStage::Toddler) .count(), - 4 + fertile_partnerships ); } @@ -2953,8 +3007,14 @@ mod tests { } } + let initial_population = world.config.initial_population; + let minimum_population = if initial_population < world.population_capacity() { + initial_population + 1 + } else { + initial_population + }; assert!( - world.people.len() >= 10, + world.people.len() >= minimum_population, "village failed to replace its initial population: daily={daily:?}, people={:?}, events={:?}", world .people @@ -2991,13 +3051,14 @@ mod tests { let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap(); - assert_eq!(restored.people.len(), 9); + let config = default_config(); + assert_eq!(restored.people.len(), config.initial_population); assert_eq!(restored.people[0].stage, LifeStage::YoungAdult); assert!( restored .people .iter() - .skip(3) + .skip(config.initial_young_adults) .all(|person| person.stage == LifeStage::Adult) ); } @@ -3009,7 +3070,8 @@ mod tests { world.sync_entities(); let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap(); - assert_eq!(restored.partnerships.len(), 4); + assert!(!world.partnerships.is_empty()); + assert_eq!(restored.partnerships.len(), world.partnerships.len()); assert_eq!(simulation_state(&restored), simulation_state(&world)); assert_eq!( serde_json::from_str::(&restored.state_json()).unwrap()["people"][0] @@ -3065,7 +3127,7 @@ mod tests { } #[test] - fn reset_keeps_seed_and_settings_with_nine_people() { + fn reset_keeps_seed_and_settings_with_initial_population() { let mut original = World::new(42); original.apply("speed 4"); original.paused = true; @@ -3076,7 +3138,7 @@ mod tests { assert_eq!(reset.seed(), 42); assert_eq!(reset.speed, 4); assert!(reset.paused); - assert_eq!(reset.people.len(), 9); + assert_eq!(reset.people.len(), default_config().initial_population); assert_eq!( reset.tick, default_config().calendar.ticks_per_day / default_config().starting_tick_fraction @@ -3168,13 +3230,18 @@ mod tests { #[test] fn rune_config_controls_initial_world_and_survives_snapshot_restore() { - let source = DEFAULT_SCRIPT - .replace("WIDTH = 96", "WIDTH = 48") - .replace("HEIGHT = 64", "HEIGHT = 32") - .replace("INITIAL_POPULATION = 9", "INITIAL_POPULATION = 6") - .replace("INITIAL_YOUNG_ADULTS = 3", "INITIAL_YOUNG_ADULTS = 2") - .replace("INITIAL_FOOD = 60", "INITIAL_FOOD = 125") - .replace("TICKS_PER_DAY = 1800", "TICKS_PER_DAY = 240"); + let source = [ + ("WIDTH", "48"), + ("HEIGHT", "32"), + ("INITIAL_POPULATION", "6"), + ("INITIAL_YOUNG_ADULTS", "2"), + ("INITIAL_FOOD", "125"), + ("TICKS_PER_DAY", "240"), + ] + .into_iter() + .fold(DEFAULT_SCRIPT.to_string(), |source, (name, value)| { + with_constant(&source, name, value) + }); let script = SimScript::load(&source).unwrap(); let world = World::new_with_script(42, script.clone()); @@ -3184,7 +3251,10 @@ mod tests { assert_eq!(world.calendar.ticks_per_day, 240); assert!(world.terrain_json().contains("\"w\":48,\"h\":32")); let state: serde_json::Value = serde_json::from_str(&world.state_json()).unwrap(); - assert_eq!(state["limits"]["speed"], 8); + assert_eq!( + state["limits"]["speed"], + default_config().max_simulation_speed + ); let restored = World::from_snapshot_with_script(&world.snapshot_json().unwrap(), 42, script).unwrap(); @@ -3195,45 +3265,52 @@ mod tests { #[test] fn restored_snapshots_use_the_provided_script() { let mut world = World::new(42); - let edited = DEFAULT_SCRIPT.replace("MAX_SIMULATION_SPEED = 8", "MAX_SIMULATION_SPEED = 3"); + let edited = with_constant(DEFAULT_SCRIPT, "MAX_SIMULATION_SPEED", "3"); world.set_script(&edited).unwrap(); assert_eq!(world.config.max_simulation_speed, 3); assert_eq!(world.script(), edited); let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap(); assert_eq!(restored.script(), DEFAULT_SCRIPT); - assert_eq!(restored.config.max_simulation_speed, 8); + assert_eq!( + restored.config.max_simulation_speed, + default_config().max_simulation_speed + ); } #[test] fn behaviours_read_script_constants() { let mut world = World::new(42); - let all_stay = DEFAULT_SCRIPT.replace( - "RABBIT_BEHAVIOR_WANDER_CHANCE = 0.5", - "RABBIT_BEHAVIOR_WANDER_CHANCE = 0.0", - ); + let all_stay = with_constant(DEFAULT_SCRIPT, "RABBIT_BEHAVIOR_WANDER_CHANCE", "0.0"); world.set_script(&all_stay).unwrap(); let c = super::Ctx { roll: 0.25, ..Default::default() }; assert_eq!(world.decide("rabbit", c.clone()), "stay"); - world.set_script(DEFAULT_SCRIPT).unwrap(); + let all_wander = with_constant(DEFAULT_SCRIPT, "RABBIT_BEHAVIOR_WANDER_CHANCE", "1.0"); + world.set_script(&all_wander).unwrap(); assert_eq!(world.decide("rabbit", c), "wander"); } #[test] fn invalid_rune_configuration_is_rejected() { - let source = DEFAULT_SCRIPT - .replace("INITIAL_POPULATION = 9", "INITIAL_POPULATION = 3") - .replace("INITIAL_YOUNG_ADULTS = 3", "INITIAL_YOUNG_ADULTS = 4"); + let source = with_constant( + &with_constant(DEFAULT_SCRIPT, "INITIAL_POPULATION", "3"), + "INITIAL_YOUNG_ADULTS", + "4", + ); assert!( SimScript::load(&source) .err() .unwrap() .contains("initial_young_adults cannot exceed initial_population") ); - let missing = DEFAULT_SCRIPT.replace("pub const WIDTH = 96;", ""); + let missing = DEFAULT_SCRIPT + .lines() + .filter(|line| !line.starts_with("pub const WIDTH = ")) + .collect::>() + .join("\n"); assert!(SimScript::load(&missing).err().unwrap().contains("WIDTH")); let no_behaviour = DEFAULT_SCRIPT.replace("pub fn wolf(c)", "pub fn wolves(c)"); assert!(SimScript::load(&no_behaviour).is_err());