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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = (self.people[first_index].x, self.people[first_index].y);
|
|
||||||
self.food -= 6;
|
self.food -= 6;
|
||||||
self.spawn_person_at(location);
|
self.spawn_person_at(self.home);
|
||||||
if let Some(partnership) = self
|
if let Some(partnership) = self
|
||||||
.partnerships
|
.partnerships
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|
@ -1181,6 +1180,9 @@ impl World {
|
||||||
p.hunger = (p.hunger - 25.0).max(0.0);
|
p.hunger = (p.hunger - 25.0).max(0.0);
|
||||||
}
|
}
|
||||||
if !p.stage.can_partner() {
|
if !p.stage.can_partner() {
|
||||||
|
if !at_home {
|
||||||
|
(p.x, p.y) = self.move_toward(pos, home);
|
||||||
|
}
|
||||||
p.action = "growing up".to_string();
|
p.action = "growing up".to_string();
|
||||||
self.people[i] = p;
|
self.people[i] = p;
|
||||||
return;
|
return;
|
||||||
|
|
@ -1437,7 +1439,21 @@ impl World {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_snapshot(snapshot: &str, fallback_seed: u64) -> Result<Self, String> {
|
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 {
|
if saved.version != 1 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unsupported world snapshot version {}",
|
"unsupported world snapshot version {}",
|
||||||
|
|
@ -1479,11 +1495,47 @@ impl World {
|
||||||
calendar: saved.calendar,
|
calendar: saved.calendar,
|
||||||
};
|
};
|
||||||
world.calendar.normalize();
|
world.calendar.normalize();
|
||||||
|
if legacy_people {
|
||||||
|
world.initialize_legacy_people();
|
||||||
|
}
|
||||||
world.prune_partnerships();
|
world.prune_partnerships();
|
||||||
world.sync_entities();
|
world.sync_entities();
|
||||||
Ok(world)
|
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)> {
|
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();
|
let n: Vec<i64> = (i..i + 4).filter_map(|k| t.get(k)?.parse().ok()).collect();
|
||||||
if n.len() != 4 {
|
if n.len() != 4 {
|
||||||
|
|
@ -1851,7 +1903,7 @@ impl Drop for World {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, LifeStage, World};
|
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, LifeStage, World, cheb};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_people_progress_through_every_stage_and_die() {
|
fn new_people_progress_through_every_stage_and_die() {
|
||||||
|
|
@ -1907,6 +1959,8 @@ mod tests {
|
||||||
.count(),
|
.count(),
|
||||||
8
|
8
|
||||||
);
|
);
|
||||||
|
world.people[0].x = 0;
|
||||||
|
world.people[0].y = 0;
|
||||||
|
|
||||||
world.sync_entities();
|
world.sync_entities();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1930,6 +1984,39 @@ mod tests {
|
||||||
.find(|person| person.stage == LifeStage::Toddler)
|
.find(|person| person.stage == LifeStage::Toddler)
|
||||||
.expect("fertile partner actors should eventually have a child");
|
.expect("fertile partner actors should eventually have a child");
|
||||||
assert_eq!(toddler.age_ticks, 0);
|
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]
|
#[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]
|
#[test]
|
||||||
fn snapshot_restores_state_and_rng() {
|
fn snapshot_restores_state_and_rng() {
|
||||||
let mut original = World::new(42);
|
let mut original = World::new(42);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue