fix(sim): keep legacy villagers and newborns alive
Restore pre-life-cycle snapshots with working-age villagers and keep dependents near the shared food stock. Add regression coverage for legacy saves, child return-home behavior, and newborn placement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
a0f18df9ef
commit
1e867b173c
110
src/world.rs
110
src/world.rs
|
|
@ -1121,9 +1121,8 @@ impl World {
|
|||
continue;
|
||||
}
|
||||
|
||||
let location = (self.people[first_index].x, self.people[first_index].y);
|
||||
self.food -= 6;
|
||||
self.spawn_person_at(location);
|
||||
self.spawn_person_at(self.home);
|
||||
if let Some(partnership) = self
|
||||
.partnerships
|
||||
.iter_mut()
|
||||
|
|
@ -1181,6 +1180,9 @@ impl World {
|
|||
p.hunger = (p.hunger - 25.0).max(0.0);
|
||||
}
|
||||
if !p.stage.can_partner() {
|
||||
if !at_home {
|
||||
(p.x, p.y) = self.move_toward(pos, home);
|
||||
}
|
||||
p.action = "growing up".to_string();
|
||||
self.people[i] = p;
|
||||
return;
|
||||
|
|
@ -1437,7 +1439,21 @@ impl World {
|
|||
}
|
||||
|
||||
pub fn from_snapshot(snapshot: &str, fallback_seed: u64) -> Result<Self, String> {
|
||||
let saved: WorldSnapshot = serde_json::from_str(snapshot).map_err(|e| e.to_string())?;
|
||||
let snapshot_value: serde_json::Value =
|
||||
serde_json::from_str(snapshot).map_err(|e| e.to_string())?;
|
||||
let legacy_people = snapshot_value
|
||||
.get("people")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|people| {
|
||||
!people.is_empty()
|
||||
&& people.iter().all(|person| {
|
||||
person
|
||||
.as_object()
|
||||
.is_some_and(|person| !person.contains_key("stage"))
|
||||
})
|
||||
});
|
||||
let saved: WorldSnapshot =
|
||||
serde_json::from_value(snapshot_value).map_err(|e| e.to_string())?;
|
||||
if saved.version != 1 {
|
||||
return Err(format!(
|
||||
"unsupported world snapshot version {}",
|
||||
|
|
@ -1479,11 +1495,47 @@ impl World {
|
|||
calendar: saved.calendar,
|
||||
};
|
||||
world.calendar.normalize();
|
||||
if legacy_people {
|
||||
world.initialize_legacy_people();
|
||||
}
|
||||
world.prune_partnerships();
|
||||
world.sync_entities();
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
fn initialize_legacy_people(&mut self) {
|
||||
for index in 0..self.people.len() {
|
||||
let stage = if index < 3 {
|
||||
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| self.sample_stage_duration(past_stage) as u64)
|
||||
.sum();
|
||||
let person = &mut self.people[index];
|
||||
person.stage = stage;
|
||||
person.stage_duration_days = duration_days;
|
||||
person.stage_elapsed_ticks = 0;
|
||||
person.age_ticks = age_days * self.calendar.ticks_per_day;
|
||||
}
|
||||
}
|
||||
|
||||
fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> {
|
||||
let n: Vec<i64> = (i..i + 4).filter_map(|k| t.get(k)?.parse().ok()).collect();
|
||||
if n.len() != 4 {
|
||||
|
|
@ -1851,7 +1903,7 @@ impl Drop for World {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, LifeStage, World};
|
||||
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, LifeStage, World, cheb};
|
||||
|
||||
#[test]
|
||||
fn new_people_progress_through_every_stage_and_die() {
|
||||
|
|
@ -1907,6 +1959,8 @@ mod tests {
|
|||
.count(),
|
||||
8
|
||||
);
|
||||
world.people[0].x = 0;
|
||||
world.people[0].y = 0;
|
||||
|
||||
world.sync_entities();
|
||||
assert_eq!(
|
||||
|
|
@ -1930,6 +1984,39 @@ mod tests {
|
|||
.find(|person| person.stage == LifeStage::Toddler)
|
||||
.expect("fertile partner actors should eventually have a child");
|
||||
assert_eq!(toddler.age_ticks, 0);
|
||||
assert_eq!((toddler.x, toddler.y), world.home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_snapshots_restore_the_village_as_adults() {
|
||||
let world = World::new(42);
|
||||
let mut snapshot: serde_json::Value =
|
||||
serde_json::from_str(&world.snapshot_json().unwrap()).unwrap();
|
||||
for person in snapshot["people"].as_array_mut().unwrap() {
|
||||
let person = person.as_object_mut().unwrap();
|
||||
for field in [
|
||||
"age_ticks",
|
||||
"stage",
|
||||
"stage_elapsed_ticks",
|
||||
"stage_duration_days",
|
||||
"partner_id",
|
||||
] {
|
||||
person.remove(field);
|
||||
}
|
||||
}
|
||||
snapshot.as_object_mut().unwrap().remove("partnerships");
|
||||
|
||||
let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap();
|
||||
|
||||
assert_eq!(restored.people.len(), 9);
|
||||
assert_eq!(restored.people[0].stage, LifeStage::YoungAdult);
|
||||
assert!(
|
||||
restored
|
||||
.people
|
||||
.iter()
|
||||
.skip(3)
|
||||
.all(|person| person.stage == LifeStage::Adult)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1948,6 +2035,21 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_return_home_instead_of_staying_away_from_food() {
|
||||
let mut world = World::new(42);
|
||||
world.food = 100;
|
||||
let home = world.home;
|
||||
let position = (home.0.saturating_add(5), home.1.saturating_add(5));
|
||||
world.spawn_person_at(position);
|
||||
|
||||
world.tick_person(world.people.len() - 1);
|
||||
|
||||
let child = world.people.last().unwrap();
|
||||
assert!(cheb((child.x, child.y), home) < cheb(position, home));
|
||||
assert_eq!(child.action, "growing up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_restores_state_and_rng() {
|
||||
let mut original = World::new(42);
|
||||
|
|
|
|||
Loading…
Reference in New Issue