feat(sim): add human life stages and partnerships
Add stage progression through death, one-to-one partnership actors, and constrained births. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ca371bd414
commit
2cb4d4e626
509
src/world.rs
509
src/world.rs
|
|
@ -21,6 +21,8 @@ pub const DEFAULT_TICKS_PER_DAY: u64 = 1800;
|
||||||
pub struct StageDuration {
|
pub struct StageDuration {
|
||||||
pub min_days: u32,
|
pub min_days: u32,
|
||||||
pub max_days: u32,
|
pub max_days: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fertile: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
@ -44,6 +46,7 @@ impl Default for StageDuration {
|
||||||
Self {
|
Self {
|
||||||
min_days: 1,
|
min_days: 1,
|
||||||
max_days: 2,
|
max_days: 2,
|
||||||
|
fertile: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,36 +61,56 @@ impl Default for CalendarSettings {
|
||||||
toddler: StageDuration {
|
toddler: StageDuration {
|
||||||
min_days: 2,
|
min_days: 2,
|
||||||
max_days: 4,
|
max_days: 4,
|
||||||
|
fertile: false,
|
||||||
},
|
},
|
||||||
child: StageDuration {
|
child: StageDuration {
|
||||||
min_days: 4,
|
min_days: 4,
|
||||||
max_days: 8,
|
max_days: 8,
|
||||||
|
fertile: false,
|
||||||
},
|
},
|
||||||
teen: StageDuration {
|
teen: StageDuration {
|
||||||
min_days: 4,
|
min_days: 4,
|
||||||
max_days: 8,
|
max_days: 8,
|
||||||
|
fertile: false,
|
||||||
},
|
},
|
||||||
young_adult: StageDuration {
|
young_adult: StageDuration {
|
||||||
min_days: 6,
|
min_days: 6,
|
||||||
max_days: 12,
|
max_days: 12,
|
||||||
|
fertile: true,
|
||||||
},
|
},
|
||||||
adult: StageDuration {
|
adult: StageDuration {
|
||||||
min_days: 20,
|
min_days: 20,
|
||||||
max_days: 40,
|
max_days: 40,
|
||||||
|
fertile: true,
|
||||||
},
|
},
|
||||||
older_adult: StageDuration {
|
older_adult: StageDuration {
|
||||||
min_days: 10,
|
min_days: 10,
|
||||||
max_days: 20,
|
max_days: 20,
|
||||||
|
fertile: false,
|
||||||
},
|
},
|
||||||
elderly: StageDuration {
|
elderly: StageDuration {
|
||||||
min_days: 4,
|
min_days: 4,
|
||||||
max_days: 10,
|
max_days: 10,
|
||||||
|
fertile: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalendarSettings {
|
impl CalendarSettings {
|
||||||
|
fn duration(&self, stage: &str) -> Option<&StageDuration> {
|
||||||
|
match stage {
|
||||||
|
"toddler" => Some(&self.toddler),
|
||||||
|
"child" => Some(&self.child),
|
||||||
|
"teen" => Some(&self.teen),
|
||||||
|
"young_adult" => Some(&self.young_adult),
|
||||||
|
"adult" => Some(&self.adult),
|
||||||
|
"older_adult" => Some(&self.older_adult),
|
||||||
|
"elderly" => Some(&self.elderly),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize(&mut self) {
|
fn normalize(&mut self) {
|
||||||
self.ticks_per_day = self.ticks_per_day.clamp(1, 1_000_000);
|
self.ticks_per_day = self.ticks_per_day.clamp(1, 1_000_000);
|
||||||
self.days_per_week = self.days_per_week.clamp(1, 100);
|
self.days_per_week = self.days_per_week.clamp(1, 100);
|
||||||
|
|
@ -122,6 +145,55 @@ impl CalendarSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum LifeStage {
|
||||||
|
#[default]
|
||||||
|
Toddler,
|
||||||
|
Child,
|
||||||
|
Teen,
|
||||||
|
YoungAdult,
|
||||||
|
Adult,
|
||||||
|
OlderAdult,
|
||||||
|
Elderly,
|
||||||
|
Dead,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LifeStage {
|
||||||
|
fn next(self) -> Option<Self> {
|
||||||
|
match self {
|
||||||
|
Self::Toddler => Some(Self::Child),
|
||||||
|
Self::Child => Some(Self::Teen),
|
||||||
|
Self::Teen => Some(Self::YoungAdult),
|
||||||
|
Self::YoungAdult => Some(Self::Adult),
|
||||||
|
Self::Adult => Some(Self::OlderAdult),
|
||||||
|
Self::OlderAdult => Some(Self::Elderly),
|
||||||
|
Self::Elderly => Some(Self::Dead),
|
||||||
|
Self::Dead => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Toddler => "toddler",
|
||||||
|
Self::Child => "child",
|
||||||
|
Self::Teen => "teen",
|
||||||
|
Self::YoungAdult => "young_adult",
|
||||||
|
Self::Adult => "adult",
|
||||||
|
Self::OlderAdult => "older_adult",
|
||||||
|
Self::Elderly => "elderly",
|
||||||
|
Self::Dead => "dead",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_partner(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::YoungAdult | Self::Adult | Self::OlderAdult | Self::Elderly
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WATER_DEEP: u8 = 0;
|
const WATER_DEEP: u8 = 0;
|
||||||
const WATER: u8 = 1;
|
const WATER: u8 = 1;
|
||||||
const SAND: u8 = 2;
|
const SAND: u8 = 2;
|
||||||
|
|
@ -275,6 +347,20 @@ pub struct Person {
|
||||||
stone: u32,
|
stone: u32,
|
||||||
action: String,
|
action: String,
|
||||||
dead: bool,
|
dead: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
age_ticks: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
stage: LifeStage,
|
||||||
|
#[serde(default)]
|
||||||
|
stage_elapsed_ticks: u64,
|
||||||
|
#[serde(default = "default_stage_duration_days")]
|
||||||
|
stage_duration_days: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
partner_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_stage_duration_days() -> u32 {
|
||||||
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Person {
|
impl Person {
|
||||||
|
|
@ -298,6 +384,18 @@ enum EntityKind {
|
||||||
Person(Person),
|
Person(Person),
|
||||||
Rabbit(Animal),
|
Rabbit(Animal),
|
||||||
Wolf(Animal),
|
Wolf(Animal),
|
||||||
|
Partnership(Partnership),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
struct Partnership {
|
||||||
|
id: u32,
|
||||||
|
first_person_id: u32,
|
||||||
|
second_person_id: u32,
|
||||||
|
formed_tick: u64,
|
||||||
|
last_birth_tick: u64,
|
||||||
|
#[serde(skip)]
|
||||||
|
actor_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -345,6 +443,8 @@ pub struct World {
|
||||||
people: Vec<Person>,
|
people: Vec<Person>,
|
||||||
rabbits: Vec<Animal>,
|
rabbits: Vec<Animal>,
|
||||||
wolves: Vec<Animal>,
|
wolves: Vec<Animal>,
|
||||||
|
partnerships: Vec<Partnership>,
|
||||||
|
next_partnership_id: u32,
|
||||||
actor_system: ActorSystem,
|
actor_system: ActorSystem,
|
||||||
entities: HashMap<u64, ActorRef<EntityMessage>>,
|
entities: HashMap<u64, ActorRef<EntityMessage>>,
|
||||||
next_actor_id: u64,
|
next_actor_id: u64,
|
||||||
|
|
@ -391,6 +491,14 @@ struct WorldSnapshot {
|
||||||
terrain_rev: u32,
|
terrain_rev: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
calendar: CalendarSettings,
|
calendar: CalendarSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
partnerships: Vec<Partnership>,
|
||||||
|
#[serde(default = "default_next_partnership_id")]
|
||||||
|
next_partnership_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_next_partnership_id() -> u32 {
|
||||||
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
|
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
|
||||||
|
|
@ -429,6 +537,8 @@ impl World {
|
||||||
people: vec![],
|
people: vec![],
|
||||||
rabbits: vec![],
|
rabbits: vec![],
|
||||||
wolves: vec![],
|
wolves: vec![],
|
||||||
|
partnerships: vec![],
|
||||||
|
next_partnership_id: 1,
|
||||||
actor_system: ActorSystem::new().expect("failed to start Picard actor system"),
|
actor_system: ActorSystem::new().expect("failed to start Picard actor system"),
|
||||||
entities: HashMap::new(),
|
entities: HashMap::new(),
|
||||||
next_actor_id: 1,
|
next_actor_id: 1,
|
||||||
|
|
@ -463,8 +573,35 @@ impl World {
|
||||||
}
|
}
|
||||||
let (hx, hy) = w.home;
|
let (hx, hy) = w.home;
|
||||||
w.res[hy * W + hx] = (0, 0);
|
w.res[hy * W + hx] = (0, 0);
|
||||||
for _ in 0..9 {
|
for i in 0..9 {
|
||||||
w.spawn_person();
|
w.spawn_person();
|
||||||
|
let stage = if i < 3 {
|
||||||
|
LifeStage::YoungAdult
|
||||||
|
} else {
|
||||||
|
LifeStage::Adult
|
||||||
|
};
|
||||||
|
let duration_days = w.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| w.sample_stage_duration(past_stage) as u64)
|
||||||
|
.sum();
|
||||||
|
let person = w.people.last_mut().expect("just spawned person");
|
||||||
|
person.stage = stage;
|
||||||
|
person.stage_duration_days = duration_days;
|
||||||
|
person.age_ticks = age_days * w.calendar.ticks_per_day;
|
||||||
}
|
}
|
||||||
for _ in 0..60 {
|
for _ in 0..60 {
|
||||||
if let Some(p) = w.random_land() {
|
if let Some(p) = w.random_land() {
|
||||||
|
|
@ -520,11 +657,13 @@ impl World {
|
||||||
self.people.clear();
|
self.people.clear();
|
||||||
self.rabbits.clear();
|
self.rabbits.clear();
|
||||||
self.wolves.clear();
|
self.wolves.clear();
|
||||||
|
self.partnerships.clear();
|
||||||
for state in states {
|
for state in states {
|
||||||
match state.kind {
|
match state.kind {
|
||||||
EntityKind::Person(person) => self.people.push(person),
|
EntityKind::Person(person) => self.people.push(person),
|
||||||
EntityKind::Rabbit(animal) => self.rabbits.push(animal),
|
EntityKind::Rabbit(animal) => self.rabbits.push(animal),
|
||||||
EntityKind::Wolf(animal) => self.wolves.push(animal),
|
EntityKind::Wolf(animal) => self.wolves.push(animal),
|
||||||
|
EntityKind::Partnership(partnership) => self.partnerships.push(partnership),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -542,6 +681,12 @@ impl World {
|
||||||
self.next_actor_id += 1;
|
self.next_actor_id += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for partnership in &mut self.partnerships {
|
||||||
|
if partnership.actor_id == 0 {
|
||||||
|
partnership.actor_id = self.next_actor_id;
|
||||||
|
self.next_actor_id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let states = self
|
let states = self
|
||||||
.people
|
.people
|
||||||
|
|
@ -558,7 +703,16 @@ impl World {
|
||||||
.chain(self.wolves.iter().cloned().map(|animal| EntityState {
|
.chain(self.wolves.iter().cloned().map(|animal| EntityState {
|
||||||
id: animal.actor_id,
|
id: animal.actor_id,
|
||||||
kind: EntityKind::Wolf(animal),
|
kind: EntityKind::Wolf(animal),
|
||||||
}));
|
}))
|
||||||
|
.chain(
|
||||||
|
self.partnerships
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|partnership| EntityState {
|
||||||
|
id: partnership.actor_id,
|
||||||
|
kind: EntityKind::Partnership(partnership),
|
||||||
|
}),
|
||||||
|
);
|
||||||
let mut active = HashSet::new();
|
let mut active = HashSet::new();
|
||||||
for state in states {
|
for state in states {
|
||||||
active.insert(state.id);
|
active.insert(state.id);
|
||||||
|
|
@ -614,7 +768,11 @@ impl World {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_person(&mut self) {
|
fn spawn_person(&mut self) {
|
||||||
let (x, y) = self.home;
|
self.spawn_person_at(self.home);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_person_at(&mut self, (x, y): (usize, usize)) {
|
||||||
|
let stage_duration_days = self.sample_stage_duration(LifeStage::Toddler);
|
||||||
self.people.push(Person {
|
self.people.push(Person {
|
||||||
actor_id: 0,
|
actor_id: 0,
|
||||||
id: self.next_id,
|
id: self.next_id,
|
||||||
|
|
@ -626,10 +784,34 @@ impl World {
|
||||||
stone: 0,
|
stone: 0,
|
||||||
action: "idle".to_string(),
|
action: "idle".to_string(),
|
||||||
dead: false,
|
dead: false,
|
||||||
|
age_ticks: 0,
|
||||||
|
stage: LifeStage::Toddler,
|
||||||
|
stage_elapsed_ticks: 0,
|
||||||
|
stage_duration_days,
|
||||||
|
partner_id: None,
|
||||||
});
|
});
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sample_stage_duration(&mut self, stage: LifeStage) -> u32 {
|
||||||
|
let duration = match stage {
|
||||||
|
LifeStage::Toddler => self.calendar.toddler,
|
||||||
|
LifeStage::Child => self.calendar.child,
|
||||||
|
LifeStage::Teen => self.calendar.teen,
|
||||||
|
LifeStage::YoungAdult => self.calendar.young_adult,
|
||||||
|
LifeStage::Adult => self.calendar.adult,
|
||||||
|
LifeStage::OlderAdult => self.calendar.older_adult,
|
||||||
|
LifeStage::Elderly => self.calendar.elderly,
|
||||||
|
LifeStage::Dead => StageDuration {
|
||||||
|
min_days: 1,
|
||||||
|
max_days: 1,
|
||||||
|
fertile: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let range = duration.max_days - duration.min_days + 1;
|
||||||
|
duration.min_days + self.rng.below(range as usize) as u32
|
||||||
|
}
|
||||||
|
|
||||||
fn say(&mut self, s: String) {
|
fn say(&mut self, s: String) {
|
||||||
self.log.push(format!(
|
self.log.push(format!(
|
||||||
"[day {}] {}",
|
"[day {}] {}",
|
||||||
|
|
@ -714,6 +896,7 @@ impl World {
|
||||||
self.load_entities();
|
self.load_entities();
|
||||||
self.tick += 1;
|
self.tick += 1;
|
||||||
self.regrow();
|
self.regrow();
|
||||||
|
self.advance_people();
|
||||||
if self.tick % 2 == 0 {
|
if self.tick % 2 == 0 {
|
||||||
for i in 0..self.people.len() {
|
for i in 0..self.people.len() {
|
||||||
self.tick_person(i);
|
self.tick_person(i);
|
||||||
|
|
@ -729,10 +912,72 @@ impl World {
|
||||||
self.people.retain(|p| !p.dead);
|
self.people.retain(|p| !p.dead);
|
||||||
self.rabbits.retain(|a| !a.dead);
|
self.rabbits.retain(|a| !a.dead);
|
||||||
self.wolves.retain(|a| !a.dead);
|
self.wolves.retain(|a| !a.dead);
|
||||||
|
self.prune_partnerships();
|
||||||
self.population();
|
self.population();
|
||||||
self.sync_entities();
|
self.sync_entities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn advance_people(&mut self) {
|
||||||
|
let mut notices = Vec::new();
|
||||||
|
for i in 0..self.people.len() {
|
||||||
|
if self.people[i].dead {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.people[i].age_ticks = self.people[i].age_ticks.saturating_add(1);
|
||||||
|
self.people[i].stage_elapsed_ticks =
|
||||||
|
self.people[i].stage_elapsed_ticks.saturating_add(1);
|
||||||
|
let duration_ticks =
|
||||||
|
self.people[i].stage_duration_days as u64 * self.calendar.ticks_per_day;
|
||||||
|
if self.people[i].stage_elapsed_ticks < duration_ticks {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let person_id = self.people[i].id;
|
||||||
|
if let Some(next) = self.people[i].stage.next() {
|
||||||
|
self.people[i].stage = next;
|
||||||
|
self.people[i].stage_elapsed_ticks = 0;
|
||||||
|
if next == LifeStage::Dead {
|
||||||
|
self.people[i].dead = true;
|
||||||
|
notices.push(format!("Person #{person_id} died of old age"));
|
||||||
|
} else {
|
||||||
|
self.people[i].stage_duration_days = self.sample_stage_duration(next);
|
||||||
|
notices.push(format!(
|
||||||
|
"Person #{person_id} became a {}",
|
||||||
|
next.key().replace('_', " ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.people[i].dead = true;
|
||||||
|
notices.push(format!("Person #{person_id} died of old age"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for notice in notices {
|
||||||
|
self.say(notice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_partnerships(&mut self) {
|
||||||
|
let alive: HashSet<u32> = self.people.iter().map(|person| person.id).collect();
|
||||||
|
self.partnerships.retain(|partnership| {
|
||||||
|
alive.contains(&partnership.first_person_id)
|
||||||
|
&& alive.contains(&partnership.second_person_id)
|
||||||
|
});
|
||||||
|
let partnered: HashSet<(u32, u32)> = self
|
||||||
|
.partnerships
|
||||||
|
.iter()
|
||||||
|
.map(|partnership| (partnership.first_person_id, partnership.second_person_id))
|
||||||
|
.flat_map(|(first, second)| [(first, second), (second, first)])
|
||||||
|
.collect();
|
||||||
|
for person in &mut self.people {
|
||||||
|
if person
|
||||||
|
.partner_id
|
||||||
|
.is_some_and(|partner| !partnered.contains(&(person.id, partner)))
|
||||||
|
{
|
||||||
|
person.partner_id = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn regrow(&mut self) {
|
fn regrow(&mut self) {
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let i = self.rng.below(W * H);
|
let i = self.rng.below(W * H);
|
||||||
|
|
@ -750,11 +995,15 @@ impl World {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn population(&mut self) {
|
fn population(&mut self) {
|
||||||
if self.tick % 400 == 0 && self.food >= 10 && self.people.len() < 8 + self.houses.len() * 3
|
let ticks_per_day = self.calendar.ticks_per_day;
|
||||||
{
|
if self.tick.is_multiple_of(ticks_per_day) {
|
||||||
self.food -= 6;
|
self.find_partners();
|
||||||
self.spawn_person();
|
}
|
||||||
self.say("A child was born".into());
|
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);
|
||||||
}
|
}
|
||||||
if self.tick % 50 == 0 {
|
if self.tick % 50 == 0 {
|
||||||
if self.rabbits.len() < 15 {
|
if self.rabbits.len() < 15 {
|
||||||
|
|
@ -784,6 +1033,110 @@ impl World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_partners(&mut self) {
|
||||||
|
for first in 0..self.people.len() {
|
||||||
|
if self.people[first].partner_id.is_some()
|
||||||
|
|| !self.people[first].stage.can_partner()
|
||||||
|
|| self.people[first].dead
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let first_position = (self.people[first].x, self.people[first].y);
|
||||||
|
let second = (0..self.people.len())
|
||||||
|
.filter(|&index| {
|
||||||
|
index != first
|
||||||
|
&& self.people[index].partner_id.is_none()
|
||||||
|
&& self.people[index].stage.can_partner()
|
||||||
|
&& !self.people[index].dead
|
||||||
|
})
|
||||||
|
.filter_map(|index| {
|
||||||
|
let distance =
|
||||||
|
cheb(first_position, (self.people[index].x, self.people[index].y));
|
||||||
|
(distance <= 12).then_some((index, distance))
|
||||||
|
})
|
||||||
|
.min_by_key(|(_, distance)| *distance)
|
||||||
|
.map(|(index, _)| index);
|
||||||
|
let Some(second) = second else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let first_id = self.people[first].id;
|
||||||
|
let second_id = self.people[second].id;
|
||||||
|
self.people[first].partner_id = Some(second_id);
|
||||||
|
self.people[second].partner_id = Some(first_id);
|
||||||
|
self.partnerships.push(Partnership {
|
||||||
|
id: self.next_partnership_id,
|
||||||
|
first_person_id: first_id,
|
||||||
|
second_person_id: second_id,
|
||||||
|
formed_tick: self.tick,
|
||||||
|
last_birth_tick: self.tick,
|
||||||
|
actor_id: 0,
|
||||||
|
});
|
||||||
|
self.next_partnership_id += 1;
|
||||||
|
self.say(format!(
|
||||||
|
"People #{first_id} and #{second_id} became partners"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn have_children(&mut self, ticks_per_month: u64) {
|
||||||
|
if self.food < 10 || self.people.len() >= 8 + self.houses.len() * 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let partnerships: Vec<_> = self
|
||||||
|
.partnerships
|
||||||
|
.iter()
|
||||||
|
.map(|partnership| {
|
||||||
|
(
|
||||||
|
partnership.id,
|
||||||
|
partnership.first_person_id,
|
||||||
|
partnership.second_person_id,
|
||||||
|
partnership.last_birth_tick,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if self.tick.saturating_sub(last_birth_tick) < ticks_per_month {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(first_index) = self.people.iter().position(|p| p.id == first_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(second_index) = self.people.iter().position(|p| p.id == second_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let first_stage = self.people[first_index].stage;
|
||||||
|
let second_stage = self.people[second_index].stage;
|
||||||
|
let can_have_children = self
|
||||||
|
.calendar
|
||||||
|
.duration(first_stage.key())
|
||||||
|
.is_some_and(|duration| duration.fertile)
|
||||||
|
&& self
|
||||||
|
.calendar
|
||||||
|
.duration(second_stage.key())
|
||||||
|
.is_some_and(|duration| duration.fertile);
|
||||||
|
if !can_have_children || !self.rng.chance(0.2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = (self.people[first_index].x, self.people[first_index].y);
|
||||||
|
self.food -= 6;
|
||||||
|
self.spawn_person_at(location);
|
||||||
|
if let Some(partnership) = self
|
||||||
|
.partnerships
|
||||||
|
.iter_mut()
|
||||||
|
.find(|partnership| partnership.id == partnership_id)
|
||||||
|
{
|
||||||
|
partnership.last_birth_tick = self.tick;
|
||||||
|
}
|
||||||
|
self.say(format!(
|
||||||
|
"People #{first_id} and #{second_id} welcomed a toddler"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decide(&mut self, func: &str, c: Ctx) -> String {
|
fn decide(&mut self, func: &str, c: Ctx) -> String {
|
||||||
match self.vm.call([func], (c,)) {
|
match self.vm.call([func], (c,)) {
|
||||||
Ok(v) => rune::from_value::<String>(v).unwrap_or_else(|_| "wander".into()),
|
Ok(v) => rune::from_value::<String>(v).unwrap_or_else(|_| "wander".into()),
|
||||||
|
|
@ -796,6 +1149,9 @@ impl World {
|
||||||
|
|
||||||
fn tick_person(&mut self, i: usize) {
|
fn tick_person(&mut self, i: usize) {
|
||||||
let mut p = self.people[i].clone();
|
let mut p = self.people[i].clone();
|
||||||
|
if p.dead {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let night = self.daylight() < 0.2;
|
let night = self.daylight() < 0.2;
|
||||||
let pos = (p.x, p.y);
|
let pos = (p.x, p.y);
|
||||||
let home = self.home;
|
let home = self.home;
|
||||||
|
|
@ -824,6 +1180,11 @@ impl World {
|
||||||
p.food -= 1;
|
p.food -= 1;
|
||||||
p.hunger = (p.hunger - 25.0).max(0.0);
|
p.hunger = (p.hunger - 25.0).max(0.0);
|
||||||
}
|
}
|
||||||
|
if !p.stage.can_partner() {
|
||||||
|
p.action = "growing up".to_string();
|
||||||
|
self.people[i] = p;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let berry = self.nearest_res(pos, BERRY, 25);
|
let berry = self.nearest_res(pos, BERRY, 25);
|
||||||
let tree = self.nearest_res(pos, TREE, 25);
|
let tree = self.nearest_res(pos, TREE, 25);
|
||||||
|
|
@ -1066,6 +1427,8 @@ impl World {
|
||||||
speed: self.speed,
|
speed: self.speed,
|
||||||
terrain_rev: self.terrain_rev,
|
terrain_rev: self.terrain_rev,
|
||||||
calendar: self.calendar.clone(),
|
calendar: self.calendar.clone(),
|
||||||
|
partnerships: self.partnerships.clone(),
|
||||||
|
next_partnership_id: self.next_partnership_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1092,6 +1455,8 @@ impl World {
|
||||||
people: saved.people,
|
people: saved.people,
|
||||||
rabbits: saved.rabbits,
|
rabbits: saved.rabbits,
|
||||||
wolves: saved.wolves,
|
wolves: saved.wolves,
|
||||||
|
partnerships: saved.partnerships,
|
||||||
|
next_partnership_id: saved.next_partnership_id,
|
||||||
actor_system: ActorSystem::new()
|
actor_system: ActorSystem::new()
|
||||||
.map_err(|e| format!("failed to start Picard actor system: {e}"))?,
|
.map_err(|e| format!("failed to start Picard actor system: {e}"))?,
|
||||||
entities: HashMap::new(),
|
entities: HashMap::new(),
|
||||||
|
|
@ -1114,6 +1479,7 @@ impl World {
|
||||||
calendar: saved.calendar,
|
calendar: saved.calendar,
|
||||||
};
|
};
|
||||||
world.calendar.normalize();
|
world.calendar.normalize();
|
||||||
|
world.prune_partnerships();
|
||||||
world.sync_entities();
|
world.sync_entities();
|
||||||
Ok(world)
|
Ok(world)
|
||||||
}
|
}
|
||||||
|
|
@ -1149,7 +1515,14 @@ impl World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("stage") => {
|
Some("stage") => {
|
||||||
if let (Some(stage), Some(min_days), Some(max_days)) = (t.get(1), num(2), num(3)) {
|
if let (Some(stage), Some(property)) = (t.get(1), t.get(2)) {
|
||||||
|
if *property == "fertile" {
|
||||||
|
if let Some(value) = num(3) {
|
||||||
|
if let Some(range) = self.calendar.duration_mut(stage) {
|
||||||
|
range.fertile = value != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let (Some(min_days), Some(max_days)) = (num(2), num(3)) {
|
||||||
let min_days = min_days.clamp(1, 1_000_000) as u32;
|
let min_days = min_days.clamp(1, 1_000_000) as u32;
|
||||||
let max_days = max_days.clamp(1, 1_000_000) as u32;
|
let max_days = max_days.clamp(1, 1_000_000) as u32;
|
||||||
if min_days <= max_days {
|
if min_days <= max_days {
|
||||||
|
|
@ -1160,6 +1533,7 @@ impl World {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Some("speed") => {
|
Some("speed") => {
|
||||||
if let Some(n) = num(1) {
|
if let Some(n) = num(1) {
|
||||||
self.speed = n.clamp(1, 8) as u32;
|
self.speed = n.clamp(1, 8) as u32;
|
||||||
|
|
@ -1304,6 +1678,7 @@ impl World {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
self.prune_partnerships();
|
||||||
self.sync_entities();
|
self.sync_entities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1400,8 +1775,8 @@ impl World {
|
||||||
}
|
}
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
s,
|
s,
|
||||||
"\"{name}\":{{\"min_days\":{},\"max_days\":{}}}",
|
"\"{name}\":{{\"min_days\":{},\"max_days\":{},\"fertile\":{}}}",
|
||||||
range.min_days, range.max_days
|
range.min_days, range.max_days, range.fertile
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
s.push_str("},");
|
s.push_str("},");
|
||||||
|
|
@ -1418,8 +1793,19 @@ impl World {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
format!(
|
format!(
|
||||||
"[{},{},{},\"{}\",{:.0},{},{},{}]",
|
"[{},{},{},\"{}\",{:.0},{},{},{},\"{}\",{},{}]",
|
||||||
p.id, p.x, p.y, p.action, p.hunger, p.food, p.wood, p.stone
|
p.id,
|
||||||
|
p.x,
|
||||||
|
p.y,
|
||||||
|
p.action,
|
||||||
|
p.hunger,
|
||||||
|
p.food,
|
||||||
|
p.wood,
|
||||||
|
p.stone,
|
||||||
|
p.stage.key(),
|
||||||
|
p.age_ticks / self.calendar.ticks_per_day,
|
||||||
|
p.partner_id
|
||||||
|
.map_or_else(|| "null".to_string(), |partner| partner.to_string())
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -1465,7 +1851,102 @@ impl Drop for World {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, World};
|
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, LifeStage, World};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_people_progress_through_every_stage_and_die() {
|
||||||
|
let mut world = World::new(42);
|
||||||
|
world.calendar.ticks_per_day = 1;
|
||||||
|
for stage in [
|
||||||
|
"toddler",
|
||||||
|
"child",
|
||||||
|
"teen",
|
||||||
|
"young_adult",
|
||||||
|
"adult",
|
||||||
|
"older_adult",
|
||||||
|
"elderly",
|
||||||
|
] {
|
||||||
|
let duration = world.calendar.duration_mut(stage).unwrap();
|
||||||
|
duration.min_days = 1;
|
||||||
|
duration.max_days = 1;
|
||||||
|
}
|
||||||
|
world.spawn_person();
|
||||||
|
let index = world.people.len() - 1;
|
||||||
|
assert_eq!(world.people[index].stage, LifeStage::Toddler);
|
||||||
|
|
||||||
|
for expected in [
|
||||||
|
LifeStage::Child,
|
||||||
|
LifeStage::Teen,
|
||||||
|
LifeStage::YoungAdult,
|
||||||
|
LifeStage::Adult,
|
||||||
|
LifeStage::OlderAdult,
|
||||||
|
LifeStage::Elderly,
|
||||||
|
LifeStage::Dead,
|
||||||
|
] {
|
||||||
|
world.advance_people();
|
||||||
|
assert_eq!(world.people[index].stage, expected);
|
||||||
|
}
|
||||||
|
assert!(world.people[index].dead);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eligible_people_form_partnership_actor_and_have_toddlers() {
|
||||||
|
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!(
|
||||||
|
world
|
||||||
|
.people
|
||||||
|
.iter()
|
||||||
|
.filter(|person| person.partner_id.is_some())
|
||||||
|
.count(),
|
||||||
|
8
|
||||||
|
);
|
||||||
|
|
||||||
|
world.sync_entities();
|
||||||
|
assert_eq!(
|
||||||
|
world.entities.len(),
|
||||||
|
world.people.len() + world.rabbits.len() + world.wolves.len() + 4
|
||||||
|
);
|
||||||
|
world.load_entities();
|
||||||
|
assert_eq!(world.partnerships.len(), 4);
|
||||||
|
|
||||||
|
let initial_population = world.people.len();
|
||||||
|
for _ in 0..100 {
|
||||||
|
world.tick += 1;
|
||||||
|
world.have_children(1);
|
||||||
|
if world.people.len() > initial_population {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let toddler = world
|
||||||
|
.people
|
||||||
|
.iter()
|
||||||
|
.find(|person| person.stage == LifeStage::Toddler)
|
||||||
|
.expect("fertile partner actors should eventually have a child");
|
||||||
|
assert_eq!(toddler.age_ticks, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partnership_and_life_stage_are_in_persisted_actor_state() {
|
||||||
|
let mut world = World::new(42);
|
||||||
|
world.find_partners();
|
||||||
|
world.sync_entities();
|
||||||
|
let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(restored.partnerships.len(), 4);
|
||||||
|
assert_eq!(restored.state_json(), world.state_json());
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<serde_json::Value>(&restored.state_json()).unwrap()["people"][0]
|
||||||
|
[8],
|
||||||
|
"young_adult"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_restores_state_and_rng() {
|
fn snapshot_restores_state_and_rng() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue