refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
mod actors;
|
|
|
|
|
|
|
|
|
|
use actors::{
|
|
|
|
|
Activity, AnimalActor, AnimalId, AnimalMsg, AnimalTurn, Area, BirthCost, Goods, Mailbox, Needs,
|
|
|
|
|
PartnershipActor, PartnershipMsg, PersonActor, PersonEdit, PersonMsg, ReplyBody, RequestId,
|
2026-10-11 15:29:17 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
use picard::actors::{ActorRef, ActorRefFactory, ActorReference, ActorSystem, Sender, Tell};
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
use rune::runtime::RuntimeContext;
|
2026-10-07 16:42:59 +00:00
|
|
|
use rune::termcolor::Buffer;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
use rune::{Any, Context, Diagnostics, Module, Source, Sources, Unit, Vm};
|
2026-10-08 06:44:30 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-10-11 16:39:50 +00:00
|
|
|
use std::cmp::Reverse;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
use std::collections::{BTreeMap, BinaryHeap, HashSet};
|
2026-10-07 16:04:37 +00:00
|
|
|
use std::fmt::Write;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
use std::sync::{Arc, OnceLock};
|
2026-10-07 16:04:37 +00:00
|
|
|
|
|
|
|
|
pub const W: usize = 96;
|
|
|
|
|
pub const H: usize = 64;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
pub const DEFAULT_SCRIPT: &str = include_str!("../config.rn");
|
2026-10-11 15:38:57 +00:00
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, rune::FromValue)]
|
2026-10-11 15:38:57 +00:00
|
|
|
pub struct StageDuration {
|
|
|
|
|
pub min_days: u32,
|
|
|
|
|
pub max_days: u32,
|
2026-10-11 15:48:26 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub fertile: bool,
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, rune::FromValue)]
|
2026-10-11 15:38:57 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub struct CalendarSettings {
|
|
|
|
|
pub ticks_per_day: u64,
|
|
|
|
|
pub days_per_week: u32,
|
|
|
|
|
pub weeks_per_month: u32,
|
|
|
|
|
pub months_per_year: u32,
|
|
|
|
|
pub toddler: StageDuration,
|
|
|
|
|
pub child: StageDuration,
|
|
|
|
|
pub teen: StageDuration,
|
|
|
|
|
pub young_adult: StageDuration,
|
|
|
|
|
pub adult: StageDuration,
|
|
|
|
|
pub older_adult: StageDuration,
|
|
|
|
|
pub elderly: StageDuration,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for StageDuration {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
min_days: 1,
|
|
|
|
|
max_days: 2,
|
2026-10-11 15:48:26 +00:00
|
|
|
fertile: false,
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for CalendarSettings {
|
|
|
|
|
fn default() -> Self {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
default_config().calendar
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, rune::FromValue)]
|
|
|
|
|
pub struct SimulationConfig {
|
|
|
|
|
width: usize,
|
|
|
|
|
height: usize,
|
|
|
|
|
calendar: CalendarSettings,
|
|
|
|
|
initial_population: usize,
|
|
|
|
|
initial_young_adults: usize,
|
|
|
|
|
initial_rabbits: usize,
|
|
|
|
|
initial_wolves: usize,
|
|
|
|
|
initial_food: u32,
|
|
|
|
|
initial_person_hunger: f32,
|
|
|
|
|
initial_animal_energy: f32,
|
|
|
|
|
animal_energy_death_threshold: f32,
|
|
|
|
|
starting_tick_fraction: u64,
|
|
|
|
|
population_capacity: usize,
|
|
|
|
|
house_capacity: usize,
|
|
|
|
|
max_houses: usize,
|
|
|
|
|
daylight_night_threshold: f32,
|
|
|
|
|
daylight_base: f32,
|
|
|
|
|
daylight_amplitude: f32,
|
|
|
|
|
home_radius: usize,
|
|
|
|
|
attack_range: usize,
|
|
|
|
|
entity_update_interval: u64,
|
|
|
|
|
hunger_night_at_home: f32,
|
|
|
|
|
hunger_other: f32,
|
|
|
|
|
hunger_eat_home_threshold: f32,
|
|
|
|
|
hunger_eat_carried_threshold: f32,
|
|
|
|
|
hunger_return_threshold: f32,
|
|
|
|
|
hunger_starvation_threshold: f32,
|
|
|
|
|
hunger_meal_reduction: f32,
|
|
|
|
|
food_per_meal: u32,
|
|
|
|
|
resource_search_radius: usize,
|
|
|
|
|
person_hunt_success_chance: f32,
|
|
|
|
|
person_hunt_food_yield: u32,
|
|
|
|
|
person_gather_yield: u16,
|
|
|
|
|
person_tooled_gather_yield: u16,
|
|
|
|
|
partnership_distance: usize,
|
|
|
|
|
birth_food_threshold: u32,
|
|
|
|
|
birth_food_cost: u32,
|
|
|
|
|
tool_wood_cost: u32,
|
|
|
|
|
tool_stone_cost: u32,
|
|
|
|
|
house_wood_cost: u32,
|
|
|
|
|
house_stone_cost: u32,
|
|
|
|
|
house_placement_radius: usize,
|
|
|
|
|
resource_regrow_attempts: usize,
|
|
|
|
|
initial_tree_chance: f32,
|
|
|
|
|
initial_tree_amount: u16,
|
|
|
|
|
initial_berry_chance: f32,
|
|
|
|
|
initial_berry_amount: u16,
|
|
|
|
|
initial_stone_chance: f32,
|
|
|
|
|
initial_stone_amount: u16,
|
|
|
|
|
tree_regrow_chance: f32,
|
|
|
|
|
tree_regrow_amount: u16,
|
|
|
|
|
berry_regrow_chance: f32,
|
|
|
|
|
berry_regrow_amount: u16,
|
|
|
|
|
stone_regrow_chance: f32,
|
|
|
|
|
stone_regrow_amount: u16,
|
|
|
|
|
animal_population_interval: u64,
|
|
|
|
|
rabbit_spawn_cap: usize,
|
|
|
|
|
wolf_spawn_cap: usize,
|
|
|
|
|
wolf_spawn_rabbit_threshold: usize,
|
|
|
|
|
wolf_min_home_distance: usize,
|
|
|
|
|
rabbit_energy_drain: f32,
|
|
|
|
|
rabbit_energy_regen: f32,
|
|
|
|
|
rabbit_energy_max: f32,
|
|
|
|
|
rabbit_wolf_detection_range: usize,
|
|
|
|
|
rabbit_breed_energy: f32,
|
|
|
|
|
rabbit_max_population: usize,
|
|
|
|
|
rabbit_breed_chance: f32,
|
|
|
|
|
rabbit_breed_energy_cost: f32,
|
|
|
|
|
rabbit_newborn_energy: f32,
|
|
|
|
|
wolf_energy_drain: f32,
|
|
|
|
|
wolf_prey_range: usize,
|
|
|
|
|
wolf_hunt_success_chance: f32,
|
|
|
|
|
wolf_hunt_energy_gain: f32,
|
|
|
|
|
wolf_energy_max: f32,
|
|
|
|
|
wolf_breed_energy: f32,
|
|
|
|
|
wolf_max_population: usize,
|
|
|
|
|
wolf_breed_chance: f32,
|
|
|
|
|
wolf_breed_energy_cost: f32,
|
|
|
|
|
wolf_newborn_energy: f32,
|
|
|
|
|
terrain_noise_octaves: usize,
|
|
|
|
|
terrain_noise_scale: f32,
|
|
|
|
|
terrain_edge_height: f32,
|
|
|
|
|
terrain_edge_falloff: f32,
|
|
|
|
|
terrain_edge_distance_cap: f32,
|
|
|
|
|
terrain_moisture_offset: f32,
|
|
|
|
|
terrain_moisture_seed_offset: u64,
|
|
|
|
|
terrain_deep_water_threshold: f32,
|
|
|
|
|
terrain_water_threshold: f32,
|
|
|
|
|
terrain_sand_threshold: f32,
|
|
|
|
|
terrain_forest_grass_threshold: f32,
|
|
|
|
|
terrain_hills_threshold: f32,
|
|
|
|
|
terrain_forest_moisture_threshold: f32,
|
|
|
|
|
random_land_attempts: usize,
|
|
|
|
|
log_entry_limit: usize,
|
|
|
|
|
wander_attempts: usize,
|
|
|
|
|
wander_radius: usize,
|
|
|
|
|
manual_spawn_limit: usize,
|
|
|
|
|
max_simulation_speed: u32,
|
|
|
|
|
max_stock_edit: u32,
|
|
|
|
|
max_person_inventory_edit: u32,
|
|
|
|
|
max_hunger_edit: i64,
|
|
|
|
|
max_animal_energy_edit: f32,
|
|
|
|
|
max_resource_edit: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SimulationConfig {
|
|
|
|
|
fn validate(&self) -> Result<(), String> {
|
2026-10-11 19:23:50 +00:00
|
|
|
self.validate_limits()?;
|
|
|
|
|
self.validate_calendar()?;
|
|
|
|
|
self.validate_chances()?;
|
|
|
|
|
self.validate_world()?;
|
|
|
|
|
self.validate_amounts()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_limits(&self) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if !(8..=256).contains(&self.width) || !(8..=256).contains(&self.height) {
|
|
|
|
|
return Err("world width and height must be between 8 and 256".into());
|
|
|
|
|
}
|
|
|
|
|
if self.initial_young_adults > self.initial_population {
|
|
|
|
|
return Err("initial_young_adults cannot exceed initial_population".into());
|
|
|
|
|
}
|
|
|
|
|
if self.food_per_meal == 0 {
|
|
|
|
|
return Err("food_per_meal must be at least 1".into());
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.population_capacity > 1_000_000 || self.house_capacity > 1_000_000 {
|
|
|
|
|
return Err("population capacity values exceed supported limits".into());
|
|
|
|
|
}
|
|
|
|
|
if self.initial_population > 10_000
|
|
|
|
|
|| self.initial_rabbits > 100_000
|
|
|
|
|
|| self.initial_wolves > 100_000
|
|
|
|
|
{
|
|
|
|
|
return Err("initial population counts exceed supported limits".into());
|
|
|
|
|
}
|
|
|
|
|
if self.max_simulation_speed == 0
|
|
|
|
|
|| self.max_simulation_speed > 32
|
|
|
|
|
|| self.max_stock_edit == 0
|
|
|
|
|
|| self.max_stock_edit > 1_000_000_000
|
|
|
|
|
|| self.max_person_inventory_edit == 0
|
|
|
|
|
|| self.max_person_inventory_edit > 1_000_000_000
|
|
|
|
|
|| self.max_hunger_edit <= 0
|
|
|
|
|
|| self.max_hunger_edit > 1_000_000_000
|
|
|
|
|
|| self.max_animal_energy_edit <= 0.0
|
|
|
|
|
|| self.max_animal_energy_edit > 1_000_000.0
|
|
|
|
|
|| self.max_resource_edit == 0
|
|
|
|
|
|| self.max_resource_edit > 1_000
|
|
|
|
|
{
|
|
|
|
|
return Err("simulation edit limits must be positive".into());
|
|
|
|
|
}
|
|
|
|
|
if self.starting_tick_fraction == 0 {
|
|
|
|
|
return Err("starting_tick_fraction must be at least 1".into());
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
fn validate_calendar(&self) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let calendar = &self.calendar;
|
|
|
|
|
if calendar.ticks_per_day == 0
|
|
|
|
|
|| calendar.ticks_per_day > 1_000_000
|
|
|
|
|
|| calendar.days_per_week == 0
|
|
|
|
|
|| calendar.days_per_week > 100
|
|
|
|
|
|| calendar.weeks_per_month == 0
|
|
|
|
|
|| calendar.weeks_per_month > 100
|
|
|
|
|
|| calendar.months_per_year == 0
|
|
|
|
|
|| calendar.months_per_year > 100
|
|
|
|
|
{
|
|
|
|
|
return Err("calendar values are outside their supported ranges".into());
|
|
|
|
|
}
|
|
|
|
|
for stage in [
|
|
|
|
|
&calendar.toddler,
|
|
|
|
|
&calendar.child,
|
|
|
|
|
&calendar.teen,
|
|
|
|
|
&calendar.young_adult,
|
|
|
|
|
&calendar.adult,
|
|
|
|
|
&calendar.older_adult,
|
|
|
|
|
&calendar.elderly,
|
|
|
|
|
] {
|
|
|
|
|
if stage.min_days == 0 || stage.min_days > stage.max_days || stage.max_days > 1_000_000
|
|
|
|
|
{
|
|
|
|
|
return Err("life-stage durations must be in the range 1..=1000000".into());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_chances(&self) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
for (name, chance) in [
|
|
|
|
|
(
|
|
|
|
|
"person_hunt_success_chance",
|
|
|
|
|
self.person_hunt_success_chance,
|
|
|
|
|
),
|
|
|
|
|
("initial_tree_chance", self.initial_tree_chance),
|
|
|
|
|
("initial_berry_chance", self.initial_berry_chance),
|
|
|
|
|
("initial_stone_chance", self.initial_stone_chance),
|
|
|
|
|
("tree_regrow_chance", self.tree_regrow_chance),
|
|
|
|
|
("berry_regrow_chance", self.berry_regrow_chance),
|
|
|
|
|
("stone_regrow_chance", self.stone_regrow_chance),
|
|
|
|
|
("rabbit_breed_chance", self.rabbit_breed_chance),
|
|
|
|
|
("wolf_breed_chance", self.wolf_breed_chance),
|
|
|
|
|
("wolf_hunt_success_chance", self.wolf_hunt_success_chance),
|
|
|
|
|
] {
|
|
|
|
|
if !(0.0..=1.0).contains(&chance) {
|
|
|
|
|
return Err(format!("{name} must be between 0 and 1"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_world(&self) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.animal_population_interval == 0 || self.entity_update_interval < 2 {
|
|
|
|
|
return Err(
|
|
|
|
|
"animal_population_interval must be positive and entity_update_interval at least 2"
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if self.terrain_noise_octaves == 0
|
|
|
|
|
|| self.terrain_noise_octaves > 16
|
|
|
|
|
|| !self.terrain_noise_scale.is_finite()
|
|
|
|
|
|| self.terrain_noise_scale <= 0.0
|
|
|
|
|
{
|
|
|
|
|
return Err("terrain_noise_octaves and terrain_noise_scale must be positive".into());
|
|
|
|
|
}
|
|
|
|
|
if self.house_placement_radius > self.width.max(self.height)
|
|
|
|
|
|| self.resource_regrow_attempts > self.width * self.height
|
|
|
|
|
|| self.random_land_attempts == 0
|
|
|
|
|
|| self.wander_attempts == 0
|
|
|
|
|
|| self.wander_radius == 0
|
|
|
|
|
|| self.wander_radius > 32
|
|
|
|
|
|| self.log_entry_limit == 0
|
|
|
|
|
{
|
|
|
|
|
return Err("world search and regrowth values exceed supported limits".into());
|
|
|
|
|
}
|
|
|
|
|
if self.terrain_deep_water_threshold >= self.terrain_water_threshold
|
|
|
|
|
|| self.terrain_water_threshold >= self.terrain_sand_threshold
|
|
|
|
|
|| self.terrain_sand_threshold >= self.terrain_forest_grass_threshold
|
|
|
|
|
|| self.terrain_forest_grass_threshold >= self.terrain_hills_threshold
|
|
|
|
|
{
|
|
|
|
|
return Err("terrain thresholds must be in ascending order".into());
|
|
|
|
|
}
|
|
|
|
|
for (name, value) in [
|
|
|
|
|
("terrain_noise_scale", self.terrain_noise_scale),
|
|
|
|
|
("terrain_edge_height", self.terrain_edge_height),
|
|
|
|
|
("terrain_edge_falloff", self.terrain_edge_falloff),
|
|
|
|
|
("terrain_edge_distance_cap", self.terrain_edge_distance_cap),
|
|
|
|
|
("terrain_moisture_offset", self.terrain_moisture_offset),
|
|
|
|
|
(
|
|
|
|
|
"terrain_deep_water_threshold",
|
|
|
|
|
self.terrain_deep_water_threshold,
|
|
|
|
|
),
|
|
|
|
|
("terrain_water_threshold", self.terrain_water_threshold),
|
|
|
|
|
("terrain_sand_threshold", self.terrain_sand_threshold),
|
|
|
|
|
(
|
|
|
|
|
"terrain_forest_grass_threshold",
|
|
|
|
|
self.terrain_forest_grass_threshold,
|
|
|
|
|
),
|
|
|
|
|
("terrain_hills_threshold", self.terrain_hills_threshold),
|
|
|
|
|
(
|
|
|
|
|
"terrain_forest_moisture_threshold",
|
|
|
|
|
self.terrain_forest_moisture_threshold,
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
if !value.is_finite() {
|
|
|
|
|
return Err(format!("{name} must be finite"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_amounts(&self) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.birth_food_cost > self.birth_food_threshold {
|
|
|
|
|
return Err("birth_food_cost cannot exceed birth_food_threshold".into());
|
|
|
|
|
}
|
|
|
|
|
for (name, value) in [
|
|
|
|
|
("initial_person_hunger", self.initial_person_hunger),
|
|
|
|
|
("initial_animal_energy", self.initial_animal_energy),
|
|
|
|
|
(
|
|
|
|
|
"animal_energy_death_threshold",
|
|
|
|
|
self.animal_energy_death_threshold,
|
|
|
|
|
),
|
|
|
|
|
("daylight_night_threshold", self.daylight_night_threshold),
|
|
|
|
|
("daylight_base", self.daylight_base),
|
|
|
|
|
("daylight_amplitude", self.daylight_amplitude),
|
|
|
|
|
("hunger_night_at_home", self.hunger_night_at_home),
|
|
|
|
|
("hunger_other", self.hunger_other),
|
|
|
|
|
("hunger_eat_home_threshold", self.hunger_eat_home_threshold),
|
|
|
|
|
(
|
|
|
|
|
"hunger_eat_carried_threshold",
|
|
|
|
|
self.hunger_eat_carried_threshold,
|
|
|
|
|
),
|
|
|
|
|
("hunger_return_threshold", self.hunger_return_threshold),
|
|
|
|
|
(
|
|
|
|
|
"hunger_starvation_threshold",
|
|
|
|
|
self.hunger_starvation_threshold,
|
|
|
|
|
),
|
|
|
|
|
("hunger_meal_reduction", self.hunger_meal_reduction),
|
|
|
|
|
("rabbit_energy_drain", self.rabbit_energy_drain),
|
|
|
|
|
("rabbit_energy_regen", self.rabbit_energy_regen),
|
|
|
|
|
("rabbit_energy_max", self.rabbit_energy_max),
|
|
|
|
|
("wolf_energy_drain", self.wolf_energy_drain),
|
|
|
|
|
("wolf_hunt_energy_gain", self.wolf_hunt_energy_gain),
|
|
|
|
|
("wolf_energy_max", self.wolf_energy_max),
|
|
|
|
|
("rabbit_breed_energy", self.rabbit_breed_energy),
|
|
|
|
|
("rabbit_breed_energy_cost", self.rabbit_breed_energy_cost),
|
|
|
|
|
("rabbit_newborn_energy", self.rabbit_newborn_energy),
|
|
|
|
|
("wolf_breed_energy", self.wolf_breed_energy),
|
|
|
|
|
("wolf_breed_energy_cost", self.wolf_breed_energy_cost),
|
|
|
|
|
("wolf_newborn_energy", self.wolf_newborn_energy),
|
|
|
|
|
] {
|
|
|
|
|
if !value.is_finite() || value < 0.0 {
|
|
|
|
|
return Err(format!("{name} must be a finite non-negative number"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CALENDAR_FIELDS: &[&str] = &[
|
|
|
|
|
"ticks_per_day",
|
|
|
|
|
"days_per_week",
|
|
|
|
|
"weeks_per_month",
|
|
|
|
|
"months_per_year",
|
|
|
|
|
"toddler",
|
|
|
|
|
"child",
|
|
|
|
|
"teen",
|
|
|
|
|
"young_adult",
|
|
|
|
|
"adult",
|
|
|
|
|
"older_adult",
|
|
|
|
|
"elderly",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// `SimulationConfig` fields (except `calendar`), each read from the `pub const`
|
|
|
|
|
/// of the same name in upper case.
|
|
|
|
|
const CONFIG_FIELDS: &[&str] = &[
|
|
|
|
|
"width",
|
|
|
|
|
"height",
|
|
|
|
|
"initial_population",
|
|
|
|
|
"initial_young_adults",
|
|
|
|
|
"initial_rabbits",
|
|
|
|
|
"initial_wolves",
|
|
|
|
|
"initial_food",
|
|
|
|
|
"initial_person_hunger",
|
|
|
|
|
"initial_animal_energy",
|
|
|
|
|
"animal_energy_death_threshold",
|
|
|
|
|
"starting_tick_fraction",
|
|
|
|
|
"population_capacity",
|
|
|
|
|
"house_capacity",
|
|
|
|
|
"max_houses",
|
|
|
|
|
"daylight_night_threshold",
|
|
|
|
|
"daylight_base",
|
|
|
|
|
"daylight_amplitude",
|
|
|
|
|
"home_radius",
|
|
|
|
|
"attack_range",
|
|
|
|
|
"entity_update_interval",
|
|
|
|
|
"hunger_night_at_home",
|
|
|
|
|
"hunger_other",
|
|
|
|
|
"hunger_eat_home_threshold",
|
|
|
|
|
"hunger_eat_carried_threshold",
|
|
|
|
|
"hunger_return_threshold",
|
|
|
|
|
"hunger_starvation_threshold",
|
|
|
|
|
"hunger_meal_reduction",
|
|
|
|
|
"food_per_meal",
|
|
|
|
|
"resource_search_radius",
|
|
|
|
|
"person_hunt_success_chance",
|
|
|
|
|
"person_hunt_food_yield",
|
|
|
|
|
"person_gather_yield",
|
|
|
|
|
"person_tooled_gather_yield",
|
|
|
|
|
"partnership_distance",
|
|
|
|
|
"birth_food_threshold",
|
|
|
|
|
"birth_food_cost",
|
|
|
|
|
"tool_wood_cost",
|
|
|
|
|
"tool_stone_cost",
|
|
|
|
|
"house_wood_cost",
|
|
|
|
|
"house_stone_cost",
|
|
|
|
|
"house_placement_radius",
|
|
|
|
|
"resource_regrow_attempts",
|
|
|
|
|
"initial_tree_chance",
|
|
|
|
|
"initial_tree_amount",
|
|
|
|
|
"initial_berry_chance",
|
|
|
|
|
"initial_berry_amount",
|
|
|
|
|
"initial_stone_chance",
|
|
|
|
|
"initial_stone_amount",
|
|
|
|
|
"tree_regrow_chance",
|
|
|
|
|
"tree_regrow_amount",
|
|
|
|
|
"berry_regrow_chance",
|
|
|
|
|
"berry_regrow_amount",
|
|
|
|
|
"stone_regrow_chance",
|
|
|
|
|
"stone_regrow_amount",
|
|
|
|
|
"animal_population_interval",
|
|
|
|
|
"rabbit_spawn_cap",
|
|
|
|
|
"wolf_spawn_cap",
|
|
|
|
|
"wolf_spawn_rabbit_threshold",
|
|
|
|
|
"wolf_min_home_distance",
|
|
|
|
|
"rabbit_energy_drain",
|
|
|
|
|
"rabbit_energy_regen",
|
|
|
|
|
"rabbit_energy_max",
|
|
|
|
|
"rabbit_wolf_detection_range",
|
|
|
|
|
"rabbit_breed_energy",
|
|
|
|
|
"rabbit_max_population",
|
|
|
|
|
"rabbit_breed_chance",
|
|
|
|
|
"rabbit_breed_energy_cost",
|
|
|
|
|
"rabbit_newborn_energy",
|
|
|
|
|
"wolf_energy_drain",
|
|
|
|
|
"wolf_prey_range",
|
|
|
|
|
"wolf_hunt_success_chance",
|
|
|
|
|
"wolf_hunt_energy_gain",
|
|
|
|
|
"wolf_energy_max",
|
|
|
|
|
"wolf_breed_energy",
|
|
|
|
|
"wolf_max_population",
|
|
|
|
|
"wolf_breed_chance",
|
|
|
|
|
"wolf_breed_energy_cost",
|
|
|
|
|
"wolf_newborn_energy",
|
|
|
|
|
"terrain_noise_octaves",
|
|
|
|
|
"terrain_noise_scale",
|
|
|
|
|
"terrain_edge_height",
|
|
|
|
|
"terrain_edge_falloff",
|
|
|
|
|
"terrain_edge_distance_cap",
|
|
|
|
|
"terrain_moisture_offset",
|
|
|
|
|
"terrain_moisture_seed_offset",
|
|
|
|
|
"terrain_deep_water_threshold",
|
|
|
|
|
"terrain_water_threshold",
|
|
|
|
|
"terrain_sand_threshold",
|
|
|
|
|
"terrain_forest_grass_threshold",
|
|
|
|
|
"terrain_hills_threshold",
|
|
|
|
|
"terrain_forest_moisture_threshold",
|
|
|
|
|
"random_land_attempts",
|
|
|
|
|
"log_entry_limit",
|
|
|
|
|
"wander_attempts",
|
|
|
|
|
"wander_radius",
|
|
|
|
|
"manual_spawn_limit",
|
|
|
|
|
"max_simulation_speed",
|
|
|
|
|
"max_stock_edit",
|
|
|
|
|
"max_person_inventory_edit",
|
|
|
|
|
"max_hunger_edit",
|
|
|
|
|
"max_animal_energy_edit",
|
|
|
|
|
"max_resource_edit",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const CONFIG_ACCESSOR: &str = "__sim_config";
|
|
|
|
|
|
|
|
|
|
/// Rune does not expose unit constants to the host, so a generated function
|
|
|
|
|
/// gathers the script's `pub const` items into one object.
|
|
|
|
|
fn config_accessor_source() -> String {
|
|
|
|
|
let fields = |names: &[&str]| {
|
|
|
|
|
names
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|name| format!("{name}: {}", name.to_uppercase()))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", ")
|
|
|
|
|
};
|
|
|
|
|
format!(
|
|
|
|
|
"pub fn {CONFIG_ACCESSOR}() {{ #{{ {}, calendar: #{{ {} }} }} }}",
|
|
|
|
|
fields(CONFIG_FIELDS),
|
|
|
|
|
fields(CALENDAR_FIELDS)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A compiled simulation script: `pub const` defaults plus `pub fn` behaviours.
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct SimScript {
|
|
|
|
|
source: String,
|
|
|
|
|
config: SimulationConfig,
|
|
|
|
|
runtime: Arc<RuntimeContext>,
|
|
|
|
|
unit: Arc<Unit>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SimScript {
|
|
|
|
|
/// Compiles the script, reads and validates its constants, and smoke-tests
|
|
|
|
|
/// every behaviour so errors surface at load time.
|
|
|
|
|
pub fn load(src: &str) -> Result<Self, String> {
|
|
|
|
|
let mut module = Module::new();
|
|
|
|
|
module.ty::<Ctx>().map_err(|e| e.to_string())?;
|
|
|
|
|
let mut context = Context::with_default_modules().map_err(|e| e.to_string())?;
|
|
|
|
|
context.install(module).map_err(|e| e.to_string())?;
|
|
|
|
|
let mut sources = Sources::new();
|
|
|
|
|
for (name, text) in [
|
|
|
|
|
("config", src.to_string()),
|
|
|
|
|
("generated", config_accessor_source()),
|
|
|
|
|
] {
|
|
|
|
|
sources
|
|
|
|
|
.insert(Source::new(name, text).map_err(|e| e.to_string())?)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
}
|
|
|
|
|
let mut diagnostics = Diagnostics::new();
|
|
|
|
|
let unit = rune::prepare(&mut sources)
|
|
|
|
|
.with_context(&context)
|
|
|
|
|
.with_diagnostics(&mut diagnostics)
|
|
|
|
|
.build();
|
2026-10-11 19:23:50 +00:00
|
|
|
let Ok(unit) = unit else {
|
|
|
|
|
let mut buffer = Buffer::no_color();
|
|
|
|
|
let _ = diagnostics.emit(&mut buffer, &sources);
|
|
|
|
|
return Err(String::from_utf8_lossy(buffer.as_slice()).into_owned());
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
};
|
|
|
|
|
let runtime = Arc::new(context.runtime().map_err(|e| e.to_string())?);
|
|
|
|
|
let unit = Arc::new(unit);
|
|
|
|
|
let mut vm = Vm::new(Arc::clone(&runtime), Arc::clone(&unit));
|
|
|
|
|
let value = vm
|
|
|
|
|
.call([CONFIG_ACCESSOR], ())
|
|
|
|
|
.map_err(|e| format!("reading constants: {e}"))?;
|
|
|
|
|
let config = rune::from_value::<SimulationConfig>(value)
|
|
|
|
|
.map_err(|e| format!("invalid constant: {e}"))?;
|
|
|
|
|
config.validate()?;
|
|
|
|
|
for f in ["person", "rabbit", "wolf"] {
|
|
|
|
|
let v = vm
|
|
|
|
|
.call([f], (Ctx::default(),))
|
|
|
|
|
.map_err(|e| format!("{f}(): {e}"))?;
|
|
|
|
|
rune::from_value::<String>(v).map_err(|_| format!("{f}() must return a string"))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(Self {
|
|
|
|
|
source: src.to_string(),
|
|
|
|
|
config,
|
|
|
|
|
runtime,
|
|
|
|
|
unit,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn source(&self) -> &str {
|
|
|
|
|
&self.source
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
|
|
|
|
|
fn vm(&self) -> Vm {
|
|
|
|
|
Vm::new(Arc::clone(&self.runtime), Arc::clone(&self.unit))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_script() -> SimScript {
|
|
|
|
|
static SCRIPT: OnceLock<SimScript> = OnceLock::new();
|
|
|
|
|
SCRIPT
|
|
|
|
|
.get_or_init(|| SimScript::load(DEFAULT_SCRIPT).expect("config.rn failed to load"))
|
|
|
|
|
.clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_config() -> SimulationConfig {
|
|
|
|
|
default_script().config
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CalendarSettings {
|
2026-10-11 15:48:26 +00:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:38:57 +00:00
|
|
|
fn normalize(&mut self) {
|
|
|
|
|
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.weeks_per_month = self.weeks_per_month.clamp(1, 100);
|
|
|
|
|
self.months_per_year = self.months_per_year.clamp(1, 100);
|
|
|
|
|
for stage in [
|
|
|
|
|
"toddler",
|
|
|
|
|
"child",
|
|
|
|
|
"teen",
|
|
|
|
|
"young_adult",
|
|
|
|
|
"adult",
|
|
|
|
|
"older_adult",
|
|
|
|
|
"elderly",
|
|
|
|
|
] {
|
|
|
|
|
let range = self.duration_mut(stage).expect("known life stage");
|
|
|
|
|
range.min_days = range.min_days.clamp(1, 1_000_000);
|
|
|
|
|
range.max_days = range.max_days.clamp(range.min_days, 1_000_000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn duration_mut(&mut self, stage: &str) -> Option<&mut StageDuration> {
|
|
|
|
|
match stage {
|
|
|
|
|
"toddler" => Some(&mut self.toddler),
|
|
|
|
|
"child" => Some(&mut self.child),
|
|
|
|
|
"teen" => Some(&mut self.teen),
|
|
|
|
|
"young_adult" => Some(&mut self.young_adult),
|
|
|
|
|
"adult" => Some(&mut self.adult),
|
|
|
|
|
"older_adult" => Some(&mut self.older_adult),
|
|
|
|
|
"elderly" => Some(&mut self.elderly),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
|
2026-10-11 15:48:26 +00:00
|
|
|
#[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
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
const WATER_DEEP: u8 = 0;
|
|
|
|
|
const WATER: u8 = 1;
|
|
|
|
|
const SAND: u8 = 2;
|
|
|
|
|
const GRASS: u8 = 3;
|
|
|
|
|
const FOREST: u8 = 4;
|
|
|
|
|
const HILLS: u8 = 5;
|
|
|
|
|
const MOUNTAIN: u8 = 6;
|
|
|
|
|
|
|
|
|
|
const TREE: u8 = 1;
|
|
|
|
|
const BERRY: u8 = 2;
|
|
|
|
|
const STONE: u8 = 3;
|
|
|
|
|
pub struct Rng(u64);
|
|
|
|
|
|
|
|
|
|
impl Rng {
|
|
|
|
|
fn next(&mut self) -> u64 {
|
|
|
|
|
let mut x = self.0;
|
|
|
|
|
x ^= x >> 12;
|
|
|
|
|
x ^= x << 25;
|
|
|
|
|
x ^= x >> 27;
|
|
|
|
|
self.0 = x;
|
2026-10-11 19:23:50 +00:00
|
|
|
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
fn f(&mut self) -> f32 {
|
2026-10-11 19:23:50 +00:00
|
|
|
unit_f32(self.next())
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
fn chance(&mut self, p: f32) -> bool {
|
|
|
|
|
self.f() < p
|
|
|
|
|
}
|
|
|
|
|
fn below(&mut self, n: usize) -> usize {
|
2026-10-11 19:23:50 +00:00
|
|
|
let n = u64::try_from(n).expect("usize fits in u64");
|
|
|
|
|
usize::try_from(self.next() % n).expect("value below a usize bound fits in usize")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Maps the top 24 bits of `bits` to `[0, 1)`.
|
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
|
|
|
fn unit_f32(bits: u64) -> f32 {
|
|
|
|
|
// Values below 2^24 are exactly representable in f32.
|
|
|
|
|
(bits >> 40) as f32 / (1u64 << 24) as f32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Converts a map coordinate or dimension (at most 256) to `f32` exactly.
|
|
|
|
|
fn coord_f32(n: usize) -> f32 {
|
|
|
|
|
f32::from(u16::try_from(n).expect("map coordinates fit in u16"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parses the integer argument at `i` of an edit command.
|
|
|
|
|
fn arg(t: &[&str], i: usize) -> Option<i64> {
|
|
|
|
|
t.get(i).and_then(|s| s.parse().ok())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Clamps `v` into `lo..=hi` and converts it; the bounds must fit in `T`.
|
|
|
|
|
fn clamp_to<T: TryFrom<i64>>(v: i64, lo: i64, hi: i64) -> T
|
|
|
|
|
where
|
|
|
|
|
T::Error: std::fmt::Debug,
|
|
|
|
|
{
|
|
|
|
|
T::try_from(v.clamp(lo, hi)).expect("edit bounds fit the target type")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Converts an edit value to `f32`, rounding values beyond 2^24 like `as` does.
|
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
|
|
|
fn small_f32(v: i64) -> f32 {
|
|
|
|
|
v as f32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sign of the step from `from` towards `to` along one axis.
|
|
|
|
|
fn direction(from: usize, to: usize) -> isize {
|
|
|
|
|
match to.cmp(&from) {
|
|
|
|
|
std::cmp::Ordering::Greater => 1,
|
|
|
|
|
std::cmp::Ordering::Less => -1,
|
|
|
|
|
std::cmp::Ordering::Equal => 0,
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn hash(x: i32, y: i32, seed: u64) -> f32 {
|
2026-10-11 19:23:50 +00:00
|
|
|
let mut h = i64::from(x)
|
|
|
|
|
.cast_unsigned()
|
|
|
|
|
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
|
|
|
|
^ i64::from(y)
|
|
|
|
|
.cast_unsigned()
|
|
|
|
|
.wrapping_mul(0xC2B2_AE3D_27D4_EB4F)
|
|
|
|
|
^ seed.wrapping_mul(0x1656_67B1_9E37_79F9);
|
2026-10-07 16:04:37 +00:00
|
|
|
h ^= h >> 33;
|
2026-10-11 19:23:50 +00:00
|
|
|
h = h.wrapping_mul(0xFF51_AFD7_ED55_8CCD);
|
2026-10-07 16:04:37 +00:00
|
|
|
h ^= h >> 33;
|
2026-10-11 19:23:50 +00:00
|
|
|
unit_f32(h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lattice cell containing `v`.
|
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
|
fn lattice(v: f32) -> i32 {
|
|
|
|
|
// Noise inputs are bounded map coordinates; `as` saturates for anything larger.
|
|
|
|
|
v.floor() as i32
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn value_noise(x: f32, y: f32, seed: u64) -> f32 {
|
2026-10-11 19:23:50 +00:00
|
|
|
let smooth = |t: f32| t * t * (3.0 - 2.0 * t);
|
|
|
|
|
let (xi, yi) = (lattice(x), lattice(y));
|
|
|
|
|
let (u, v) = (smooth(x - x.floor()), smooth(y - y.floor()));
|
|
|
|
|
let top_left = hash(xi, yi, seed);
|
|
|
|
|
let top_right = hash(xi + 1, yi, seed);
|
|
|
|
|
let bottom_left = hash(xi, yi + 1, seed);
|
|
|
|
|
let bottom_right = hash(xi + 1, yi + 1, seed);
|
|
|
|
|
let top = top_left + (top_right - top_left) * u;
|
|
|
|
|
let bottom = bottom_left + (bottom_right - bottom_left) * u;
|
|
|
|
|
top + (bottom - top) * v
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
fn fbm(x: f32, y: f32, seed: u64, octaves: usize, scale: f32) -> f32 {
|
|
|
|
|
let (mut sum, mut amp, mut freq, mut norm) = (0.0, 1.0, 1.0 / scale, 0.0);
|
|
|
|
|
for o in 0..octaves {
|
|
|
|
|
sum += amp * value_noise(x * freq, y * freq, seed + o as u64);
|
2026-10-07 16:04:37 +00:00
|
|
|
norm += amp;
|
|
|
|
|
amp *= 0.5;
|
|
|
|
|
freq *= 2.0;
|
|
|
|
|
}
|
|
|
|
|
sum / norm
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:42:59 +00:00
|
|
|
/// What an entity senses this tick; passed to the Rune behaviour scripts.
|
2026-10-11 19:23:50 +00:00
|
|
|
// A flat record of independent sensor flags keeps field access simple in `config.rn`.
|
|
|
|
|
#[allow(clippy::struct_excessive_bools)]
|
2026-10-07 16:42:59 +00:00
|
|
|
#[derive(Any, Default, Clone)]
|
|
|
|
|
struct Ctx {
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
night: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
at_home: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
hunger: f64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
carry: i64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
carried_food: i64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
stock_food: i64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
stock_wood: i64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
stock_stone: i64,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
berry_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
tree_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
stone_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
rabbit_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
wolf_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
prey_near: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
roll: f64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:29:17 +00:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
2026-10-07 16:04:37 +00:00
|
|
|
pub struct Person {
|
|
|
|
|
id: u32,
|
|
|
|
|
x: usize,
|
|
|
|
|
y: usize,
|
|
|
|
|
hunger: f32,
|
|
|
|
|
food: u32,
|
|
|
|
|
wood: u32,
|
|
|
|
|
stone: u32,
|
2026-10-08 06:44:30 +00:00
|
|
|
action: String,
|
2026-10-07 16:04:37 +00:00
|
|
|
dead: bool,
|
2026-10-11 15:48:26 +00:00
|
|
|
#[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>,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// State of the person's own random stream, used for their life-stage durations.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
rng: u64,
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_stage_duration_days() -> u32 {
|
|
|
|
|
1
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Person {
|
|
|
|
|
fn carry(&self) -> u32 {
|
|
|
|
|
self.food + self.wood + self.stone
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
|
|
|
|
|
fn position(&self) -> (usize, usize) {
|
|
|
|
|
(self.x, self.y)
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:29:17 +00:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
2026-10-07 16:04:37 +00:00
|
|
|
pub struct Animal {
|
|
|
|
|
x: usize,
|
|
|
|
|
y: usize,
|
|
|
|
|
energy: f32,
|
|
|
|
|
dead: bool,
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
impl Animal {
|
|
|
|
|
fn position(&self) -> (usize, usize) {
|
|
|
|
|
(self.x, self.y)
|
|
|
|
|
}
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
|
|
|
struct Partnership {
|
|
|
|
|
id: u32,
|
|
|
|
|
first_person_id: u32,
|
|
|
|
|
second_person_id: u32,
|
|
|
|
|
formed_tick: u64,
|
|
|
|
|
last_birth_tick: u64,
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
enum Species {
|
|
|
|
|
Rabbit,
|
|
|
|
|
Wolf,
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// The entity state observed from the actors at the start of a step, kept up to date with
|
|
|
|
|
/// their replies while the step runs. It is a read model only; the actors own the state.
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct Census {
|
|
|
|
|
people: Vec<Person>,
|
|
|
|
|
rabbits: Vec<(AnimalId, Animal)>,
|
|
|
|
|
wolves: Vec<(AnimalId, Animal)>,
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
impl Census {
|
|
|
|
|
fn animals_mut(&mut self, species: Species) -> &mut Vec<(AnimalId, Animal)> {
|
|
|
|
|
match species {
|
|
|
|
|
Species::Rabbit => &mut self.rabbits,
|
|
|
|
|
Species::Wolf => &mut self.wolves,
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// World-level state (terrain, resources, the shared stockpile, houses, calendar and the world
|
|
|
|
|
/// random stream) lives here. Every person, animal and partnership is a Picard actor that owns
|
|
|
|
|
/// its own state; the world only coordinates them, because the Rune VM that decides behaviours
|
|
|
|
|
/// cannot leave the simulation thread.
|
2026-10-07 16:04:37 +00:00
|
|
|
pub struct World {
|
2026-10-08 06:58:34 +00:00
|
|
|
seed: u64,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
width: usize,
|
|
|
|
|
height: usize,
|
2026-10-07 16:04:37 +00:00
|
|
|
tiles: Vec<u8>,
|
|
|
|
|
res: Vec<(u8, u16)>,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
person_actors: BTreeMap<u32, ActorRef<PersonMsg>>,
|
|
|
|
|
rabbit_actors: BTreeMap<AnimalId, ActorRef<AnimalMsg>>,
|
|
|
|
|
wolf_actors: BTreeMap<AnimalId, ActorRef<AnimalMsg>>,
|
|
|
|
|
partnership_actors: BTreeMap<u32, ActorRef<PartnershipMsg>>,
|
|
|
|
|
next_animal_id: AnimalId,
|
2026-10-11 15:48:26 +00:00
|
|
|
next_partnership_id: u32,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
mailbox: Mailbox,
|
2026-10-11 15:29:17 +00:00
|
|
|
actor_system: ActorSystem,
|
2026-10-07 16:04:37 +00:00
|
|
|
houses: Vec<(usize, usize)>,
|
|
|
|
|
home: (usize, usize),
|
|
|
|
|
food: u32,
|
|
|
|
|
wood: u32,
|
|
|
|
|
stone: u32,
|
|
|
|
|
tools: u32,
|
|
|
|
|
rng: Rng,
|
|
|
|
|
tick: u64,
|
|
|
|
|
next_id: u32,
|
|
|
|
|
log: Vec<String>,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
script: SimScript,
|
2026-10-07 16:42:59 +00:00
|
|
|
vm: Vm,
|
|
|
|
|
pub paused: bool,
|
|
|
|
|
pub speed: u32,
|
|
|
|
|
pub terrain_rev: u32,
|
2026-10-11 15:38:57 +00:00
|
|
|
calendar: CalendarSettings,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
config: SimulationConfig,
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:44:30 +00:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
struct WorldSnapshot {
|
|
|
|
|
version: u32,
|
2026-10-08 06:58:34 +00:00
|
|
|
seed: Option<u64>,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
#[serde(default = "default_width")]
|
|
|
|
|
width: usize,
|
|
|
|
|
#[serde(default = "default_height")]
|
|
|
|
|
height: usize,
|
2026-10-08 06:44:30 +00:00
|
|
|
tiles: Vec<u8>,
|
|
|
|
|
res: Vec<(u8, u16)>,
|
|
|
|
|
people: Vec<Person>,
|
|
|
|
|
rabbits: Vec<Animal>,
|
|
|
|
|
wolves: Vec<Animal>,
|
|
|
|
|
houses: Vec<(usize, usize)>,
|
|
|
|
|
home: (usize, usize),
|
|
|
|
|
food: u32,
|
|
|
|
|
wood: u32,
|
|
|
|
|
stone: u32,
|
|
|
|
|
tools: u32,
|
|
|
|
|
rng: u64,
|
|
|
|
|
tick: u64,
|
|
|
|
|
next_id: u32,
|
|
|
|
|
log: Vec<String>,
|
|
|
|
|
paused: bool,
|
|
|
|
|
speed: u32,
|
|
|
|
|
terrain_rev: u32,
|
2026-10-11 15:38:57 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
calendar: CalendarSettings,
|
2026-10-11 15:48:26 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
partnerships: Vec<Partnership>,
|
|
|
|
|
#[serde(default = "default_next_partnership_id")]
|
|
|
|
|
next_partnership_id: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_next_partnership_id() -> u32 {
|
|
|
|
|
1
|
2026-10-08 06:44:30 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
fn default_width() -> usize {
|
|
|
|
|
W
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_height() -> usize {
|
|
|
|
|
H
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
|
|
|
|
|
a.0.abs_diff(b.0).max(a.1.abs_diff(b.1))
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn sample_stage_duration(calendar: &CalendarSettings, stage: LifeStage, rng: &mut Rng) -> u32 {
|
|
|
|
|
let duration = match stage {
|
|
|
|
|
LifeStage::Toddler => calendar.toddler,
|
|
|
|
|
LifeStage::Child => calendar.child,
|
|
|
|
|
LifeStage::Teen => calendar.teen,
|
|
|
|
|
LifeStage::YoungAdult => calendar.young_adult,
|
|
|
|
|
LifeStage::Adult => calendar.adult,
|
|
|
|
|
LifeStage::OlderAdult => calendar.older_adult,
|
|
|
|
|
LifeStage::Elderly => 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
|
|
|
|
|
+ u32::try_from(rng.below(range as usize)).expect("sample is below a u32 range")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Drops partnerships with a missing member and clears partner links without a partnership.
|
|
|
|
|
fn repair_partnerships(people: &mut [Person], partnerships: &mut Vec<Partnership>) {
|
|
|
|
|
let alive: HashSet<u32> = people.iter().map(|person| person.id).collect();
|
|
|
|
|
partnerships.retain(|partnership| {
|
|
|
|
|
alive.contains(&partnership.first_person_id)
|
|
|
|
|
&& alive.contains(&partnership.second_person_id)
|
|
|
|
|
});
|
|
|
|
|
let partnered: HashSet<(u32, u32)> = partnerships
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|partnership| (partnership.first_person_id, partnership.second_person_id))
|
|
|
|
|
.flat_map(|(first, second)| [(first, second), (second, first)])
|
|
|
|
|
.collect();
|
|
|
|
|
for person in people {
|
|
|
|
|
if person
|
|
|
|
|
.partner_id
|
|
|
|
|
.is_some_and(|partner| !partnered.contains(&(person.id, partner)))
|
|
|
|
|
{
|
|
|
|
|
person.partner_id = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Collects one reply per actor and sorts them by key, so the simulation stays deterministic
|
|
|
|
|
/// whatever order the actors answered in.
|
|
|
|
|
fn sorted_by_key<K: Ord, T>(mut items: Vec<(K, T)>) -> Vec<(K, T)> {
|
|
|
|
|
items.sort_by(|a, b| a.0.cmp(&b.0));
|
|
|
|
|
items
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
impl World {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
#[cfg(test)]
|
2026-10-07 16:04:37 +00:00
|
|
|
pub fn new(seed: u64) -> World {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
Self::new_with_script(seed, default_script())
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
fn generate_terrain(seed: u64, config: &SimulationConfig) -> Vec<u8> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let (width, height) = (config.width, config.height);
|
|
|
|
|
let mut tiles = vec![0u8; width * height];
|
|
|
|
|
for y in 0..height {
|
|
|
|
|
for x in 0..width {
|
2026-10-11 19:23:50 +00:00
|
|
|
let (fx, fy) = (coord_f32(x), coord_f32(y));
|
|
|
|
|
let dx = (fx / coord_f32(width) - 0.5) * 2.0;
|
|
|
|
|
let dy = (fy / coord_f32(height) - 0.5) * 2.0;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let d = (dx * dx + dy * dy).min(config.terrain_edge_distance_cap);
|
|
|
|
|
let h = fbm(
|
2026-10-11 19:23:50 +00:00
|
|
|
fx,
|
|
|
|
|
fy,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
seed,
|
|
|
|
|
config.terrain_noise_octaves,
|
|
|
|
|
config.terrain_noise_scale,
|
|
|
|
|
) + config.terrain_edge_height
|
|
|
|
|
- config.terrain_edge_falloff * d;
|
|
|
|
|
let m = fbm(
|
2026-10-11 19:23:50 +00:00
|
|
|
fx + config.terrain_moisture_offset,
|
|
|
|
|
fy + config.terrain_moisture_offset,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
seed.wrapping_add(config.terrain_moisture_seed_offset),
|
|
|
|
|
config.terrain_noise_octaves,
|
|
|
|
|
config.terrain_noise_scale,
|
|
|
|
|
);
|
|
|
|
|
tiles[y * width + x] = if h < config.terrain_deep_water_threshold {
|
2026-10-07 16:04:37 +00:00
|
|
|
WATER_DEEP
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
} else if h < config.terrain_water_threshold {
|
2026-10-07 16:04:37 +00:00
|
|
|
WATER
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
} else if h < config.terrain_sand_threshold {
|
2026-10-07 16:04:37 +00:00
|
|
|
SAND
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
} else if h < config.terrain_forest_grass_threshold {
|
|
|
|
|
if m > config.terrain_forest_moisture_threshold {
|
|
|
|
|
FOREST
|
|
|
|
|
} else {
|
|
|
|
|
GRASS
|
|
|
|
|
}
|
|
|
|
|
} else if h < config.terrain_hills_threshold {
|
2026-10-07 16:04:37 +00:00
|
|
|
HILLS
|
|
|
|
|
} else {
|
|
|
|
|
MOUNTAIN
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
tiles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn populate(&mut self) {
|
|
|
|
|
for i in 0..self.config.initial_population {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut person = self.new_person(self.home);
|
2026-10-11 19:23:50 +00:00
|
|
|
let stage = if i < self.config.initial_young_adults {
|
|
|
|
|
LifeStage::YoungAdult
|
|
|
|
|
} else {
|
|
|
|
|
LifeStage::Adult
|
|
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.make_grown_up(&mut person, stage);
|
|
|
|
|
self.spawn_person_actor(person);
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
for _ in 0..self.config.initial_rabbits {
|
|
|
|
|
if let Some(p) = self.random_land() {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let energy = self.config.initial_animal_energy;
|
|
|
|
|
self.spawn_animal(Species::Rabbit, p, energy);
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _ in 0..self.config.initial_wolves {
|
|
|
|
|
if let Some(p) = self.random_land()
|
|
|
|
|
&& cheb(p, self.home) > self.config.wolf_min_home_distance
|
|
|
|
|
{
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let energy = self.config.initial_animal_energy;
|
|
|
|
|
self.spawn_animal(Species::Wolf, p, energy);
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// Gives `person` the age of someone who has lived through every stage before `stage`.
|
|
|
|
|
fn make_grown_up(&mut self, person: &mut Person, stage: LifeStage) {
|
|
|
|
|
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| u64::from(self.sample_stage_duration(past_stage)))
|
|
|
|
|
.sum();
|
|
|
|
|
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 empty(seed: u64, script: SimScript, tiles: Vec<u8>, res: Vec<(u8, u16)>) -> World {
|
2026-10-11 19:23:50 +00:00
|
|
|
let config = script.config.clone();
|
|
|
|
|
let (width, height) = (config.width, config.height);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let actor_system = ActorSystem::new().expect("failed to start Picard actor system");
|
|
|
|
|
World {
|
2026-10-08 06:58:34 +00:00
|
|
|
seed,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
width,
|
|
|
|
|
height,
|
2026-10-07 16:04:37 +00:00
|
|
|
tiles,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
res,
|
|
|
|
|
person_actors: BTreeMap::new(),
|
|
|
|
|
rabbit_actors: BTreeMap::new(),
|
|
|
|
|
wolf_actors: BTreeMap::new(),
|
|
|
|
|
partnership_actors: BTreeMap::new(),
|
|
|
|
|
next_animal_id: 1,
|
2026-10-11 15:48:26 +00:00
|
|
|
next_partnership_id: 1,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
mailbox: Mailbox::new(&actor_system),
|
|
|
|
|
actor_system,
|
2026-10-07 16:04:37 +00:00
|
|
|
houses: vec![],
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
home: (width / 2, height / 2),
|
|
|
|
|
food: config.initial_food,
|
2026-10-07 16:04:37 +00:00
|
|
|
wood: 0,
|
|
|
|
|
stone: 0,
|
|
|
|
|
tools: 0,
|
2026-10-11 19:23:50 +00:00
|
|
|
rng: Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1),
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
tick: (config.calendar.ticks_per_day / config.starting_tick_fraction).max(1),
|
2026-10-07 16:04:37 +00:00
|
|
|
next_id: 1,
|
|
|
|
|
log: vec![],
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
vm: script.vm(),
|
|
|
|
|
script,
|
2026-10-07 16:42:59 +00:00
|
|
|
paused: false,
|
|
|
|
|
speed: 1,
|
|
|
|
|
terrain_rev: 0,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
calendar: config.calendar.clone(),
|
|
|
|
|
config,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn new_with_script(seed: u64, script: SimScript) -> World {
|
|
|
|
|
let tiles = Self::generate_terrain(seed, &script.config);
|
|
|
|
|
let res = vec![(0, 0); tiles.len()];
|
|
|
|
|
let mut w = Self::empty(seed, script, tiles, res);
|
|
|
|
|
let (width, height) = (w.width, w.height);
|
2026-10-07 16:04:37 +00:00
|
|
|
w.home = w
|
|
|
|
|
.find_land(GRASS)
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
.unwrap_or_else(|| w.find_land(SAND).unwrap_or((width / 2, height / 2)));
|
|
|
|
|
for i in 0..width * height {
|
2026-10-07 16:04:37 +00:00
|
|
|
let r = w.rng.f();
|
|
|
|
|
w.res[i] = match w.tiles[i] {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
FOREST if r < w.config.initial_tree_chance => (TREE, w.config.initial_tree_amount),
|
|
|
|
|
GRASS if r < w.config.initial_berry_chance => {
|
|
|
|
|
(BERRY, w.config.initial_berry_amount)
|
|
|
|
|
}
|
|
|
|
|
HILLS if r < w.config.initial_stone_chance => {
|
|
|
|
|
(STONE, w.config.initial_stone_amount)
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
_ => (0, 0),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
let (hx, hy) = w.home;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
w.res[hy * width + hx] = (0, 0);
|
2026-10-11 19:23:50 +00:00
|
|
|
w.populate();
|
2026-10-07 16:04:37 +00:00
|
|
|
w
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn spawn_person_actor(&mut self, person: Person) {
|
|
|
|
|
let id = person.id;
|
|
|
|
|
let actor = self
|
|
|
|
|
.actor_system
|
|
|
|
|
.actor_of_args::<PersonActor, _>(&format!("person-{id}"), person)
|
|
|
|
|
.expect("failed to create Picard person actor");
|
|
|
|
|
self.person_actors.insert(id, actor);
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn animal_actors(&self, species: Species) -> &BTreeMap<AnimalId, ActorRef<AnimalMsg>> {
|
|
|
|
|
match species {
|
|
|
|
|
Species::Rabbit => &self.rabbit_actors,
|
|
|
|
|
Species::Wolf => &self.wolf_actors,
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animal_actors_mut(
|
|
|
|
|
&mut self,
|
|
|
|
|
species: Species,
|
|
|
|
|
) -> &mut BTreeMap<AnimalId, ActorRef<AnimalMsg>> {
|
|
|
|
|
match species {
|
|
|
|
|
Species::Rabbit => &mut self.rabbit_actors,
|
|
|
|
|
Species::Wolf => &mut self.wolf_actors,
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn spawn_animal_actor(&mut self, species: Species, animal: Animal) -> AnimalId {
|
|
|
|
|
let id = self.next_animal_id;
|
|
|
|
|
self.next_animal_id += 1;
|
|
|
|
|
let kind = match species {
|
|
|
|
|
Species::Rabbit => "rabbit",
|
|
|
|
|
Species::Wolf => "wolf",
|
|
|
|
|
};
|
|
|
|
|
let actor = self
|
|
|
|
|
.actor_system
|
|
|
|
|
.actor_of_args::<AnimalActor, _>(&format!("{kind}-{id}"), (id, animal))
|
|
|
|
|
.expect("failed to create Picard animal actor");
|
|
|
|
|
self.animal_actors_mut(species).insert(id, actor);
|
|
|
|
|
id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_animal(
|
|
|
|
|
&mut self,
|
|
|
|
|
species: Species,
|
|
|
|
|
(x, y): (usize, usize),
|
|
|
|
|
energy: f32,
|
|
|
|
|
) -> (AnimalId, Animal) {
|
|
|
|
|
let animal = Animal {
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
energy,
|
|
|
|
|
dead: false,
|
|
|
|
|
};
|
|
|
|
|
(self.spawn_animal_actor(species, animal.clone()), animal)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_partnership_actor(&mut self, partnership: Partnership) {
|
|
|
|
|
let id = partnership.id;
|
|
|
|
|
let actor = self
|
|
|
|
|
.actor_system
|
|
|
|
|
.actor_of_args::<PartnershipActor, _>(&format!("partnership-{id}"), partnership)
|
|
|
|
|
.expect("failed to create Picard partnership actor");
|
|
|
|
|
self.partnership_actors.insert(id, actor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn retire_person(&mut self, id: u32) {
|
|
|
|
|
if let Some(actor) = self.person_actors.remove(&id) {
|
|
|
|
|
actor.stop();
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn retire_animal(&mut self, species: Species, id: AnimalId) {
|
|
|
|
|
if let Some(actor) = self.animal_actors_mut(species).remove(&id) {
|
|
|
|
|
actor.stop();
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn retire_partnership(&mut self, id: u32) {
|
|
|
|
|
if let Some(actor) = self.partnership_actors.remove(&id) {
|
|
|
|
|
actor.stop();
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sends one message to every actor in `actors` and waits for all replies.
|
|
|
|
|
fn broadcast<K, M: picard::LocalMessage>(
|
|
|
|
|
&self,
|
|
|
|
|
actors: &BTreeMap<K, ActorRef<M>>,
|
|
|
|
|
message: impl Fn(RequestId) -> M,
|
|
|
|
|
) -> Vec<ReplyBody> {
|
|
|
|
|
if actors.is_empty() {
|
|
|
|
|
return Vec::new();
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let request = self.mailbox.request();
|
|
|
|
|
for actor in actors.values() {
|
|
|
|
|
actor.tell(message(request), self.mailbox.reply_to());
|
|
|
|
|
}
|
|
|
|
|
self.mailbox.collect(request, actors.len())
|
|
|
|
|
}
|
2026-10-11 15:29:17 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn observe_people(&self) -> Vec<Person> {
|
|
|
|
|
let people = self
|
|
|
|
|
.broadcast(&self.person_actors, PersonMsg::Observe)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::Person(person) => (person.id, person),
|
|
|
|
|
other => panic!("person actor answered an observation with {other:?}"),
|
2026-10-11 15:29:17 +00:00
|
|
|
})
|
2026-10-12 10:43:05 +00:00
|
|
|
.collect();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
sorted_by_key(people).into_iter().map(|(_, p)| p).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observe_animals(&self, species: Species) -> Vec<(AnimalId, Animal)> {
|
|
|
|
|
let animals = self
|
|
|
|
|
.broadcast(self.animal_actors(species), AnimalMsg::Observe)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::Animal(id, animal) => (id, animal),
|
|
|
|
|
other => panic!("animal actor answered an observation with {other:?}"),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
sorted_by_key(animals)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observe_partnerships(&self) -> Vec<Partnership> {
|
|
|
|
|
let partnerships = self
|
|
|
|
|
.broadcast(&self.partnership_actors, PartnershipMsg::Observe)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::Partnership(partnership) => (partnership.id, partnership),
|
|
|
|
|
other => panic!("partnership actor answered an observation with {other:?}"),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
sorted_by_key(partnerships)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(_, p)| p)
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn census(&self) -> Census {
|
|
|
|
|
Census {
|
|
|
|
|
people: self.observe_people(),
|
|
|
|
|
rabbits: self.observe_animals(Species::Rabbit),
|
|
|
|
|
wolves: self.observe_animals(Species::Wolf),
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tell_person(&self, id: u32, message: PersonMsg) {
|
|
|
|
|
if let Some(actor) = self.person_actors.get(&id) {
|
|
|
|
|
actor.tell(message, None);
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sends a request to one actor and waits for its reply.
|
|
|
|
|
fn ask<M: picard::LocalMessage>(
|
|
|
|
|
&self,
|
|
|
|
|
actor: &ActorRef<M>,
|
|
|
|
|
message: impl FnOnce(RequestId) -> M,
|
|
|
|
|
) -> ReplyBody {
|
|
|
|
|
self.mailbox
|
|
|
|
|
.ask(|request, sender: Sender| actor.tell(message(request), sender))
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
fn find_land(&self, kind: u8) -> Option<(usize, usize)> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let c = (self.width / 2, self.height / 2);
|
2026-10-07 16:04:37 +00:00
|
|
|
let mut best: Option<(usize, usize)> = None;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
for y in 0..self.height {
|
|
|
|
|
for x in 0..self.width {
|
|
|
|
|
if self.tiles[y * self.width + x] == kind
|
2026-10-07 16:04:37 +00:00
|
|
|
&& best.is_none_or(|b| cheb((x, y), c) < cheb(b, c))
|
|
|
|
|
{
|
|
|
|
|
best = Some((x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
best
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn random_land(&mut self) -> Option<(usize, usize)> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
for _ in 0..self.config.random_land_attempts {
|
|
|
|
|
let (x, y) = (self.rng.below(self.width), self.rng.below(self.height));
|
2026-10-11 19:23:50 +00:00
|
|
|
if self.walkable((x, y)) {
|
2026-10-07 16:04:37 +00:00
|
|
|
return Some((x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn new_person(&mut self, (x, y): (usize, usize)) -> Person {
|
2026-10-11 15:48:26 +00:00
|
|
|
let stage_duration_days = self.sample_stage_duration(LifeStage::Toddler);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let person = Person {
|
2026-10-07 16:04:37 +00:00
|
|
|
id: self.next_id,
|
|
|
|
|
x,
|
|
|
|
|
y,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
hunger: self.config.initial_person_hunger,
|
2026-10-07 16:04:37 +00:00
|
|
|
food: 0,
|
|
|
|
|
wood: 0,
|
|
|
|
|
stone: 0,
|
2026-10-08 06:44:30 +00:00
|
|
|
action: "idle".to_string(),
|
2026-10-07 16:04:37 +00:00
|
|
|
dead: false,
|
2026-10-11 15:48:26 +00:00
|
|
|
age_ticks: 0,
|
|
|
|
|
stage: LifeStage::Toddler,
|
|
|
|
|
stage_elapsed_ticks: 0,
|
|
|
|
|
stage_duration_days,
|
|
|
|
|
partner_id: None,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
rng: self.rng.next() | 1,
|
|
|
|
|
};
|
2026-10-07 16:04:37 +00:00
|
|
|
self.next_id += 1;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
person
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_person_at(&mut self, position: (usize, usize)) -> Person {
|
|
|
|
|
let person = self.new_person(position);
|
|
|
|
|
self.spawn_person_actor(person.clone());
|
|
|
|
|
person
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:48:26 +00:00
|
|
|
fn sample_stage_duration(&mut self, stage: LifeStage) -> u32 {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
sample_stage_duration(&self.calendar, stage, &mut self.rng)
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn population_u32(&self) -> u32 {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
u32::try_from(self.person_actors.len()).unwrap_or(u32::MAX)
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
fn say(&mut self, s: &str) {
|
2026-10-11 15:38:57 +00:00
|
|
|
self.log.push(format!(
|
|
|
|
|
"[day {}] {}",
|
|
|
|
|
self.tick / self.calendar.ticks_per_day + 1,
|
|
|
|
|
s
|
|
|
|
|
));
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.log.len() > self.config.log_entry_limit {
|
2026-10-07 16:04:37 +00:00
|
|
|
self.log.remove(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
/// Fraction of the current day that has elapsed, in `[0, 1)`.
|
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
|
|
|
fn day_fraction(&self) -> f32 {
|
|
|
|
|
// `ticks_per_day` is validated to at most 1,000,000, which f32 represents exactly.
|
2026-10-11 15:38:57 +00:00
|
|
|
let ticks_per_day = self.calendar.ticks_per_day;
|
2026-10-11 19:23:50 +00:00
|
|
|
(self.tick % ticks_per_day) as f32 / ticks_per_day as f32
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn daylight(&self) -> f32 {
|
|
|
|
|
let phase = self.day_fraction();
|
2026-10-07 16:04:37 +00:00
|
|
|
let v = -(phase * std::f32::consts::TAU).cos();
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
(self.config.daylight_base + v * self.config.daylight_amplitude).clamp(0.0, 1.0)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
fn walkable(&self, (x, y): (usize, usize)) -> bool {
|
|
|
|
|
x < self.width
|
|
|
|
|
&& y < self.height
|
|
|
|
|
&& matches!(
|
|
|
|
|
self.tiles[y * self.width + x],
|
|
|
|
|
SAND | GRASS | FOREST | HILLS
|
|
|
|
|
)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
/// The in-bounds tile at `(x + dx, y + dy)`, if any.
|
|
|
|
|
fn offset(&self, (x, y): (usize, usize), dx: isize, dy: isize) -> Option<(usize, usize)> {
|
|
|
|
|
let x = x.checked_add_signed(dx).filter(|&x| x < self.width)?;
|
|
|
|
|
let y = y.checked_add_signed(dy).filter(|&y| y < self.height)?;
|
|
|
|
|
Some((x, y))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The walkable tile at `(x + dx, y + dy)`, excluding the origin itself.
|
|
|
|
|
fn walk_to(&self, from: (usize, usize), dx: isize, dy: isize) -> Option<(usize, usize)> {
|
|
|
|
|
if (dx, dy) == (0, 0) {
|
|
|
|
|
return None;
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
self.offset(from, dx, dy).filter(|&to| self.walkable(to))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn move_dir(&mut self, x: usize, y: usize, dx: isize, dy: isize) -> (usize, usize) {
|
|
|
|
|
[(dx, dy), (dx, 0), (0, dy)]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find_map(|(a, b)| self.walk_to((x, y), a, b))
|
|
|
|
|
.unwrap_or_else(|| self.wander(x, y))
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wander(&mut self, x: usize, y: usize) -> (usize, usize) {
|
2026-10-11 19:23:50 +00:00
|
|
|
let radius = self.config.wander_radius;
|
|
|
|
|
let diameter = radius * 2 + 1;
|
|
|
|
|
let signed_radius = isize::try_from(radius).expect("wander radius is validated");
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
for _ in 0..self.config.wander_attempts {
|
2026-10-11 19:23:50 +00:00
|
|
|
let a = isize::try_from(self.rng.below(diameter)).expect("bounded by diameter");
|
|
|
|
|
let b = isize::try_from(self.rng.below(diameter)).expect("bounded by diameter");
|
|
|
|
|
if let Some(to) = self.walk_to((x, y), a - signed_radius, b - signed_radius) {
|
|
|
|
|
return to;
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(x, y)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn move_toward(&mut self, from: (usize, usize), to: (usize, usize)) -> (usize, usize) {
|
2026-10-11 16:39:50 +00:00
|
|
|
if from == to {
|
|
|
|
|
return from;
|
|
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let start = from.1 * self.width + from.0;
|
|
|
|
|
let target = to.1 * self.width + to.0;
|
2026-10-11 16:39:50 +00:00
|
|
|
let mut frontier = BinaryHeap::from([Reverse((cheb(from, to), start))]);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let mut costs = vec![usize::MAX; self.width * self.height];
|
|
|
|
|
let mut previous = vec![usize::MAX; self.width * self.height];
|
2026-10-11 16:39:50 +00:00
|
|
|
costs[start] = 0;
|
|
|
|
|
previous[start] = start;
|
|
|
|
|
|
|
|
|
|
while let Some(Reverse((_, current))) = frontier.pop() {
|
|
|
|
|
if current == target {
|
|
|
|
|
let mut next = target;
|
|
|
|
|
while previous[next] != start {
|
|
|
|
|
next = previous[next];
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
return (next % self.width, next / self.width);
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let position = (current % self.width, current / self.width);
|
2026-10-11 16:39:50 +00:00
|
|
|
for dy in -1..=1 {
|
|
|
|
|
for dx in -1..=1 {
|
|
|
|
|
if (dx, dy) == (0, 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
let Some((x, y)) = self.walk_to(position, dx, dy) else {
|
2026-10-11 16:39:50 +00:00
|
|
|
continue;
|
2026-10-11 19:23:50 +00:00
|
|
|
};
|
|
|
|
|
let neighbor = y * self.width + x;
|
2026-10-11 16:39:50 +00:00
|
|
|
let cost = costs[current] + 1;
|
|
|
|
|
if cost >= costs[neighbor] {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
costs[neighbor] = cost;
|
|
|
|
|
previous[neighbor] = current;
|
2026-10-11 19:23:50 +00:00
|
|
|
frontier.push(Reverse((cost + cheb((x, y), to), neighbor)));
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.wander(from.0, from.1)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nearest_res(&self, from: (usize, usize), kind: u8, r: usize) -> Option<(usize, usize)> {
|
|
|
|
|
let mut best: Option<(usize, usize)> = None;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let (x0, x1) = (
|
|
|
|
|
from.0.saturating_sub(r),
|
|
|
|
|
from.0.saturating_add(r).min(self.width - 1),
|
|
|
|
|
);
|
|
|
|
|
let (y0, y1) = (
|
|
|
|
|
from.1.saturating_sub(r),
|
|
|
|
|
from.1.saturating_add(r).min(self.height - 1),
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.res[y * self.width + x].0 == kind
|
2026-10-07 16:04:37 +00:00
|
|
|
&& best.is_none_or(|b| cheb((x, y), from) < cheb(b, from))
|
|
|
|
|
{
|
|
|
|
|
best = Some((x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
best
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn nearest_animal(
|
|
|
|
|
animals: &[(AnimalId, Animal)],
|
|
|
|
|
from: (usize, usize),
|
|
|
|
|
r: usize,
|
|
|
|
|
) -> Option<usize> {
|
2026-10-07 16:04:37 +00:00
|
|
|
let mut best: Option<(usize, usize)> = None;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for (i, (_, a)) in animals.iter().enumerate() {
|
|
|
|
|
let d = cheb(a.position(), from);
|
2026-10-07 16:04:37 +00:00
|
|
|
if !a.dead && d <= r && best.is_none_or(|(_, bd)| d < bd) {
|
|
|
|
|
best = Some((i, d));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
best.map(|b| b.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn step(&mut self) {
|
|
|
|
|
self.tick += 1;
|
|
|
|
|
self.regrow();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut census = Census {
|
|
|
|
|
people: self.advance_people(),
|
|
|
|
|
rabbits: self.observe_animals(Species::Rabbit),
|
|
|
|
|
wolves: self.observe_animals(Species::Wolf),
|
|
|
|
|
};
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.tick.is_multiple_of(self.config.entity_update_interval) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.tick_people(&mut census);
|
|
|
|
|
self.tick_rabbits(&mut census);
|
2026-10-07 16:04:37 +00:00
|
|
|
} else {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.tick_wolves(&mut census);
|
|
|
|
|
}
|
|
|
|
|
if self.retire_dead(&mut census) {
|
|
|
|
|
self.prune_partnerships(&mut census.people);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.population(&mut census);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// Ages every person by one tick and returns them, logging stage changes and deaths.
|
|
|
|
|
fn advance_people(&mut self) -> Vec<Person> {
|
|
|
|
|
let calendar = self.calendar.clone();
|
|
|
|
|
let replies = self.broadcast(&self.person_actors, |request| {
|
|
|
|
|
PersonMsg::Age(request, calendar.clone())
|
|
|
|
|
});
|
|
|
|
|
let aged = replies
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::Aged(person, stage) => (person.id, (person, stage)),
|
|
|
|
|
other => panic!("person actor answered ageing with {other:?}"),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let mut people = Vec::with_capacity(self.person_actors.len());
|
|
|
|
|
for (id, (person, stage)) in sorted_by_key(aged) {
|
|
|
|
|
match stage {
|
|
|
|
|
Some(LifeStage::Dead) => self.say(&format!("Person #{id} died of old age")),
|
|
|
|
|
Some(stage) => self.say(&format!(
|
|
|
|
|
"Person #{id} became a {}",
|
|
|
|
|
stage.key().replace('_', " ")
|
|
|
|
|
)),
|
|
|
|
|
None => {}
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
people.push(person);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
people
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// Stops the actors of dead entities; returns whether any person died.
|
|
|
|
|
fn retire_dead(&mut self, census: &mut Census) -> bool {
|
|
|
|
|
let dead_people: Vec<u32> = census
|
|
|
|
|
.people
|
2026-10-11 15:48:26 +00:00
|
|
|
.iter()
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
.filter(|person| person.dead)
|
|
|
|
|
.map(|person| person.id)
|
2026-10-11 15:48:26 +00:00
|
|
|
.collect();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for &id in &dead_people {
|
|
|
|
|
self.retire_person(id);
|
|
|
|
|
}
|
|
|
|
|
census.people.retain(|person| !person.dead);
|
|
|
|
|
for species in [Species::Rabbit, Species::Wolf] {
|
|
|
|
|
let dead: Vec<AnimalId> = census
|
|
|
|
|
.animals_mut(species)
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(_, animal)| animal.dead)
|
|
|
|
|
.map(|(id, _)| *id)
|
|
|
|
|
.collect();
|
|
|
|
|
for id in dead {
|
|
|
|
|
self.retire_animal(species, id);
|
|
|
|
|
}
|
|
|
|
|
census
|
|
|
|
|
.animals_mut(species)
|
|
|
|
|
.retain(|(_, animal)| !animal.dead);
|
|
|
|
|
}
|
|
|
|
|
!dead_people.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dissolves partnerships that lost a member and tells their survivors.
|
|
|
|
|
fn prune_partnerships(&mut self, people: &mut [Person]) {
|
|
|
|
|
let mut partnerships = self.observe_partnerships();
|
|
|
|
|
let before: HashSet<u32> = partnerships.iter().map(|p| p.id).collect();
|
|
|
|
|
let partners_before: Vec<Option<u32>> = people.iter().map(|p| p.partner_id).collect();
|
|
|
|
|
repair_partnerships(people, &mut partnerships);
|
|
|
|
|
let kept: HashSet<u32> = partnerships.iter().map(|p| p.id).collect();
|
|
|
|
|
for id in before.difference(&kept) {
|
|
|
|
|
self.retire_partnership(*id);
|
|
|
|
|
}
|
|
|
|
|
for (person, partner_before) in people.iter().zip(partners_before) {
|
|
|
|
|
if person.partner_id != partner_before {
|
|
|
|
|
self.tell_person(person.id, PersonMsg::SetPartner(person.partner_id));
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
fn regrow(&mut self) {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
for _ in 0..self.config.resource_regrow_attempts {
|
|
|
|
|
let i = self.rng.below(self.width * self.height);
|
2026-10-07 16:04:37 +00:00
|
|
|
if self.res[i].0 != 0 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let p = self.rng.f();
|
|
|
|
|
match self.tiles[i] {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
FOREST if p < self.config.tree_regrow_chance => {
|
2026-10-11 19:23:50 +00:00
|
|
|
self.res[i] = (TREE, self.config.tree_regrow_amount);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
}
|
|
|
|
|
GRASS if p < self.config.berry_regrow_chance => {
|
2026-10-11 19:23:50 +00:00
|
|
|
self.res[i] = (BERRY, self.config.berry_regrow_amount);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
}
|
|
|
|
|
HILLS if p < self.config.stone_regrow_chance => {
|
2026-10-11 19:23:50 +00:00
|
|
|
self.res[i] = (STONE, self.config.stone_regrow_amount);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn population(&mut self, census: &mut Census) {
|
2026-10-11 15:48:26 +00:00
|
|
|
let ticks_per_day = self.calendar.ticks_per_day;
|
|
|
|
|
if self.tick.is_multiple_of(ticks_per_day) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.find_partners(&mut census.people);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
let ticks_per_week = ticks_per_day * u64::from(self.calendar.days_per_week);
|
2026-10-11 16:13:10 +00:00
|
|
|
if self.tick.is_multiple_of(ticks_per_week) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.have_children(ticks_per_week, census);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self
|
|
|
|
|
.tick
|
|
|
|
|
.is_multiple_of(self.config.animal_population_interval)
|
|
|
|
|
{
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let energy = self.config.initial_animal_energy;
|
|
|
|
|
if census.rabbits.len() < self.config.rabbit_spawn_cap
|
2026-10-11 18:42:29 +00:00
|
|
|
&& let Some(p) = self.random_land()
|
|
|
|
|
{
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let rabbit = self.spawn_animal(Species::Rabbit, p, energy);
|
|
|
|
|
census.rabbits.push(rabbit);
|
|
|
|
|
}
|
|
|
|
|
if census.wolves.len() < self.config.wolf_spawn_cap
|
|
|
|
|
&& census.rabbits.len() > self.config.wolf_spawn_rabbit_threshold
|
2026-10-11 18:42:29 +00:00
|
|
|
&& let Some(p) = self.random_land()
|
|
|
|
|
&& cheb(p, self.home) > self.config.wolf_min_home_distance
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
{
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let wolf = self.spawn_animal(Species::Wolf, p, energy);
|
|
|
|
|
census.wolves.push(wolf);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn find_partners(&mut self, people: &mut [Person]) {
|
2026-10-11 19:23:50 +00:00
|
|
|
let ticks_per_week = self.calendar.ticks_per_day * u64::from(self.calendar.days_per_week);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for first in 0..people.len() {
|
|
|
|
|
if people[first].partner_id.is_some()
|
|
|
|
|
|| !people[first].stage.can_partner()
|
|
|
|
|
|| people[first].dead
|
2026-10-11 15:48:26 +00:00
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let first_position = people[first].position();
|
|
|
|
|
let second = (0..people.len())
|
2026-10-11 15:48:26 +00:00
|
|
|
.filter(|&index| {
|
|
|
|
|
index != first
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
&& people[index].partner_id.is_none()
|
|
|
|
|
&& people[index].stage.can_partner()
|
|
|
|
|
&& !people[index].dead
|
2026-10-11 15:48:26 +00:00
|
|
|
})
|
|
|
|
|
.filter_map(|index| {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let distance = cheb(first_position, people[index].position());
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
(distance <= self.config.partnership_distance).then_some((index, distance))
|
2026-10-11 15:48:26 +00:00
|
|
|
})
|
|
|
|
|
.min_by_key(|(_, distance)| *distance)
|
|
|
|
|
.map(|(index, _)| index);
|
|
|
|
|
let Some(second) = second else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let first_id = people[first].id;
|
|
|
|
|
let second_id = people[second].id;
|
|
|
|
|
people[first].partner_id = Some(second_id);
|
|
|
|
|
people[second].partner_id = Some(first_id);
|
|
|
|
|
self.tell_person(first_id, PersonMsg::SetPartner(Some(second_id)));
|
|
|
|
|
self.tell_person(second_id, PersonMsg::SetPartner(Some(first_id)));
|
|
|
|
|
self.spawn_partnership_actor(Partnership {
|
2026-10-11 15:48:26 +00:00
|
|
|
id: self.next_partnership_id,
|
|
|
|
|
first_person_id: first_id,
|
|
|
|
|
second_person_id: second_id,
|
|
|
|
|
formed_tick: self.tick,
|
2026-10-11 16:39:50 +00:00
|
|
|
last_birth_tick: self.tick / ticks_per_week * ticks_per_week,
|
2026-10-11 15:48:26 +00:00
|
|
|
});
|
|
|
|
|
self.next_partnership_id += 1;
|
2026-10-11 19:23:50 +00:00
|
|
|
self.say(&format!(
|
2026-10-11 15:48:26 +00:00
|
|
|
"People #{first_id} and #{second_id} became partners"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn have_children(&mut self, birth_interval_ticks: u64, census: &mut Census) {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.food < self.config.birth_food_threshold
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
|| census.people.len() >= self.population_capacity()
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
{
|
2026-10-11 15:48:26 +00:00
|
|
|
return;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let tick = self.tick;
|
|
|
|
|
let replies = self.broadcast(&self.partnership_actors, |request| {
|
|
|
|
|
PartnershipMsg::BirthDue {
|
|
|
|
|
request,
|
|
|
|
|
tick,
|
|
|
|
|
interval: birth_interval_ticks,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
let due = replies
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::BirthDue(partnership, due) => (partnership.id, (partnership, due)),
|
|
|
|
|
other => panic!("partnership actor answered a birth check with {other:?}"),
|
2026-10-11 15:48:26 +00:00
|
|
|
})
|
|
|
|
|
.collect();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for (partnership_id, (partnership, due)) in sorted_by_key(due) {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.food < self.config.birth_food_threshold
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
|| census.people.len() >= self.population_capacity()
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
{
|
2026-10-11 15:48:26 +00:00
|
|
|
break;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
if !due {
|
2026-10-11 15:48:26 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let (first_id, second_id) = (partnership.first_person_id, partnership.second_person_id);
|
|
|
|
|
let fertile = |id: u32| {
|
|
|
|
|
census
|
|
|
|
|
.people
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|person| person.id == id)
|
|
|
|
|
.and_then(|person| self.calendar.duration(person.stage.key()))
|
|
|
|
|
.is_some_and(|duration| duration.fertile)
|
2026-10-11 15:48:26 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
if !(fertile(first_id) && fertile(second_id)) {
|
2026-10-11 15:48:26 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.food -= self.config.birth_food_cost;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let toddler = self.spawn_person_at(self.home);
|
|
|
|
|
census.people.push(toddler);
|
|
|
|
|
if let Some(actor) = self.partnership_actors.get(&partnership_id) {
|
|
|
|
|
actor.tell(PartnershipMsg::RecordBirth(self.tick), None);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
self.say(&format!(
|
2026-10-11 15:48:26 +00:00
|
|
|
"People #{first_id} and #{second_id} welcomed a toddler"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:13:10 +00:00
|
|
|
fn population_capacity(&self) -> usize {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.config.population_capacity + self.houses.len() * self.config.house_capacity
|
2026-10-11 16:13:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:42:59 +00:00
|
|
|
fn decide(&mut self, func: &str, c: Ctx) -> String {
|
|
|
|
|
match self.vm.call([func], (c,)) {
|
|
|
|
|
Ok(v) => rune::from_value::<String>(v).unwrap_or_else(|_| "wander".into()),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("script error in {func}: {e}");
|
|
|
|
|
"wander".into()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn tick_people(&mut self, census: &mut Census) {
|
|
|
|
|
let performed = self.mailbox.request();
|
|
|
|
|
let mut performers = 0;
|
|
|
|
|
for index in 0..census.people.len() {
|
|
|
|
|
if census.people[index].dead {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let Some(person) = self.meet_needs(&census.people[index]) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
census.people[index] = person.clone();
|
|
|
|
|
if person.dead {
|
|
|
|
|
self.say(&format!("Person #{} starved", person.id));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let activity = self.choose_activity(&person, census);
|
|
|
|
|
if let Some(actor) = self.person_actors.get(&person.id) {
|
|
|
|
|
actor.tell(
|
|
|
|
|
PersonMsg::Perform(performed, activity),
|
|
|
|
|
self.mailbox.reply_to(),
|
|
|
|
|
);
|
|
|
|
|
performers += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for reply in self.mailbox.collect(performed, performers) {
|
|
|
|
|
let ReplyBody::Person(person) = reply else {
|
|
|
|
|
panic!("person actor answered an activity with {reply:?}");
|
|
|
|
|
};
|
|
|
|
|
if let Some(entry) = census.people.iter_mut().find(|p| p.id == person.id) {
|
|
|
|
|
*entry = person;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lets a person get hungry and eat, settling deposits and meals with the stockpile.
|
|
|
|
|
fn meet_needs(&mut self, person: &Person) -> Option<Person> {
|
|
|
|
|
let actor = self.person_actors.get(&person.id)?;
|
|
|
|
|
let at_home = cheb(person.position(), self.home) <= self.config.home_radius;
|
|
|
|
|
let night = self.daylight() < self.config.daylight_night_threshold;
|
|
|
|
|
let needs = Needs {
|
|
|
|
|
at_home,
|
|
|
|
|
hunger_gain: if night && at_home {
|
|
|
|
|
self.config.hunger_night_at_home
|
|
|
|
|
} else {
|
|
|
|
|
self.config.hunger_other
|
|
|
|
|
},
|
|
|
|
|
stock_food: self.food,
|
|
|
|
|
home_meal_threshold: self.config.hunger_eat_home_threshold,
|
|
|
|
|
carried_meal_threshold: self.config.hunger_eat_carried_threshold,
|
|
|
|
|
starvation_threshold: self.config.hunger_starvation_threshold,
|
|
|
|
|
meal_reduction: self.config.hunger_meal_reduction,
|
|
|
|
|
food_per_meal: self.config.food_per_meal,
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let reply = self.ask(actor, |request| PersonMsg::Needs(request, needs));
|
|
|
|
|
let ReplyBody::Fed(person, outcome) = reply else {
|
|
|
|
|
panic!("person actor answered its needs with {reply:?}");
|
|
|
|
|
};
|
|
|
|
|
self.food += outcome.deposited.food;
|
|
|
|
|
self.wood += outcome.deposited.wood;
|
|
|
|
|
self.stone += outcome.deposited.stone;
|
|
|
|
|
if outcome.ate_from_stock {
|
|
|
|
|
self.food -= self.config.food_per_meal;
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
if at_home {
|
2026-10-07 16:42:59 +00:00
|
|
|
self.craft_and_build();
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
Some(person)
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Harvests `kind` at `target` when standing on it, otherwise walks there.
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn gather(
|
|
|
|
|
&mut self,
|
|
|
|
|
pos: (usize, usize),
|
|
|
|
|
kind: u8,
|
|
|
|
|
target: (usize, usize),
|
|
|
|
|
) -> ((usize, usize), Goods) {
|
2026-10-11 19:23:50 +00:00
|
|
|
let idx = pos.1 * self.width + pos.0;
|
|
|
|
|
if pos != target || self.res[idx].0 != kind {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
return (self.move_toward(pos, target), Goods::default());
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
let yield_n: u16 = if self.tools >= self.population_u32() {
|
|
|
|
|
self.config.person_tooled_gather_yield
|
|
|
|
|
} else {
|
|
|
|
|
self.config.person_gather_yield
|
|
|
|
|
};
|
|
|
|
|
let take = yield_n.min(self.res[idx].1);
|
|
|
|
|
self.res[idx].1 -= take;
|
|
|
|
|
if self.res[idx].1 == 0 {
|
|
|
|
|
self.res[idx].0 = 0;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let take = u32::from(take);
|
|
|
|
|
let gathered = match kind {
|
|
|
|
|
BERRY => Goods {
|
|
|
|
|
food: take,
|
|
|
|
|
..Goods::default()
|
|
|
|
|
},
|
|
|
|
|
TREE => Goods {
|
|
|
|
|
wood: take,
|
|
|
|
|
..Goods::default()
|
|
|
|
|
},
|
|
|
|
|
_ => Goods {
|
|
|
|
|
stone: take,
|
|
|
|
|
..Goods::default()
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
(pos, gathered)
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// Asks a rabbit actor to suffer a successful attack; returns whether it died of it.
|
|
|
|
|
fn hunt_rabbit(&self, census: &mut Census, index: usize) -> bool {
|
|
|
|
|
let (id, _) = census.rabbits[index];
|
|
|
|
|
let Some(actor) = self.rabbit_actors.get(&id) else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
let reply = self.ask(actor, AnimalMsg::Hunted);
|
|
|
|
|
let ReplyBody::Hunted(_, killed) = reply else {
|
|
|
|
|
panic!("rabbit actor answered a hunt with {reply:?}");
|
|
|
|
|
};
|
|
|
|
|
census.rabbits[index].1.dead = true;
|
|
|
|
|
killed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Decides what a fed person does this tick using the `person` behaviour script.
|
|
|
|
|
fn choose_activity(&mut self, p: &Person, census: &mut Census) -> Activity {
|
2026-10-11 19:23:50 +00:00
|
|
|
let night = self.daylight() < self.config.daylight_night_threshold;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let pos = p.position();
|
2026-10-11 19:23:50 +00:00
|
|
|
let home = self.home;
|
|
|
|
|
let at_home = cheb(pos, home) <= self.config.home_radius;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let activity = |action: &str, position| Activity {
|
|
|
|
|
action: action.to_string(),
|
|
|
|
|
position,
|
|
|
|
|
gathered: Goods::default(),
|
|
|
|
|
};
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if p.hunger > self.config.hunger_return_threshold && self.food > 0 && !at_home {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
return activity("returning for food", self.move_toward(pos, home));
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
2026-10-11 15:48:26 +00:00
|
|
|
if !p.stage.can_partner() {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let position = if at_home {
|
|
|
|
|
pos
|
|
|
|
|
} else {
|
|
|
|
|
self.move_toward(pos, home)
|
|
|
|
|
};
|
|
|
|
|
return activity("growing up", position);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let berry = self.nearest_res(pos, BERRY, self.config.resource_search_radius);
|
|
|
|
|
let tree = self.nearest_res(pos, TREE, self.config.resource_search_radius);
|
|
|
|
|
let stone = self.nearest_res(pos, STONE, self.config.resource_search_radius);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let rabbit = Self::nearest_animal(&census.rabbits, pos, self.config.resource_search_radius);
|
2026-10-11 19:23:50 +00:00
|
|
|
let ctx = Ctx {
|
2026-10-07 16:42:59 +00:00
|
|
|
night,
|
|
|
|
|
at_home,
|
2026-10-11 19:23:50 +00:00
|
|
|
hunger: f64::from(p.hunger),
|
|
|
|
|
carry: i64::from(p.carry()),
|
|
|
|
|
carried_food: i64::from(p.food),
|
|
|
|
|
stock_food: i64::from(self.food),
|
|
|
|
|
stock_wood: i64::from(self.wood),
|
|
|
|
|
stock_stone: i64::from(self.stone),
|
2026-10-07 16:42:59 +00:00
|
|
|
berry_near: berry.is_some(),
|
|
|
|
|
tree_near: tree.is_some(),
|
|
|
|
|
stone_near: stone.is_some(),
|
|
|
|
|
rabbit_near: rabbit.is_some(),
|
2026-10-11 19:23:50 +00:00
|
|
|
roll: f64::from(self.rng.f()),
|
2026-10-07 16:42:59 +00:00
|
|
|
..Default::default()
|
|
|
|
|
};
|
2026-10-11 19:23:50 +00:00
|
|
|
let choice = self.decide("person", ctx);
|
2026-10-07 16:42:59 +00:00
|
|
|
let gather = match choice.as_str() {
|
|
|
|
|
"forage" => Some((BERRY, berry, "foraging")),
|
|
|
|
|
"chop" => Some((TREE, tree, "chopping wood")),
|
|
|
|
|
"mine" => Some((STONE, stone, "mining stone")),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
match (choice.as_str(), gather, rabbit) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
("sleep", ..) => activity("sleeping", pos),
|
2026-10-07 16:42:59 +00:00
|
|
|
("go_home" | "haul" | "eat", ..) => {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let action = match choice.as_str() {
|
2026-10-07 16:42:59 +00:00
|
|
|
"go_home" => "going home",
|
|
|
|
|
"haul" => "hauling goods",
|
|
|
|
|
_ => "going to eat",
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
};
|
|
|
|
|
let position = if at_home {
|
|
|
|
|
pos
|
|
|
|
|
} else {
|
|
|
|
|
self.move_toward(pos, home)
|
|
|
|
|
};
|
|
|
|
|
activity(action, position)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
("hunt", _, Some(rabbit)) => {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let target = census.rabbits[rabbit].1.position();
|
|
|
|
|
if cheb(pos, target) > self.config.attack_range {
|
|
|
|
|
return activity("hunting", self.move_toward(pos, target));
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut hunting = activity("hunting", pos);
|
|
|
|
|
if self.rng.chance(self.config.person_hunt_success_chance)
|
|
|
|
|
&& self.hunt_rabbit(census, rabbit)
|
|
|
|
|
{
|
|
|
|
|
hunting.gathered.food = self.config.person_hunt_food_yield;
|
|
|
|
|
}
|
|
|
|
|
hunting
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
(_, Some((kind, Some(target), label)), _) => {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let (position, gathered) = self.gather(pos, kind, target);
|
|
|
|
|
Activity {
|
|
|
|
|
action: label.to_string(),
|
|
|
|
|
position,
|
|
|
|
|
gathered,
|
|
|
|
|
}
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
_ => activity("wandering", self.wander(pos.0, pos.1)),
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:42:59 +00:00
|
|
|
fn craft_and_build(&mut self) {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.wood >= self.config.tool_wood_cost
|
|
|
|
|
&& self.stone >= self.config.tool_stone_cost
|
2026-10-11 19:23:50 +00:00
|
|
|
&& self.tools < self.population_u32()
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
{
|
|
|
|
|
self.wood -= self.config.tool_wood_cost;
|
|
|
|
|
self.stone -= self.config.tool_stone_cost;
|
2026-10-07 16:42:59 +00:00
|
|
|
self.tools += 1;
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if self.wood >= self.config.house_wood_cost
|
|
|
|
|
&& self.stone >= self.config.house_stone_cost
|
|
|
|
|
&& self.houses.len() < self.config.max_houses
|
|
|
|
|
{
|
2026-10-07 16:42:59 +00:00
|
|
|
let (hx, hy) = self.home;
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let radius = self.config.house_placement_radius;
|
|
|
|
|
let diameter = radius * 2 + 1;
|
2026-10-11 19:23:50 +00:00
|
|
|
let x = (hx + self.rng.below(diameter))
|
|
|
|
|
.saturating_sub(radius)
|
|
|
|
|
.min(self.width - 1);
|
|
|
|
|
let y = (hy + self.rng.below(diameter))
|
|
|
|
|
.saturating_sub(radius)
|
|
|
|
|
.min(self.height - 1);
|
|
|
|
|
if self.walkable((x, y))
|
|
|
|
|
&& self.res[y * self.width + x].0 == 0
|
|
|
|
|
&& !self.houses.contains(&(x, y))
|
2026-10-07 16:42:59 +00:00
|
|
|
{
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.wood -= self.config.house_wood_cost;
|
|
|
|
|
self.stone -= self.config.house_stone_cost;
|
2026-10-11 19:23:50 +00:00
|
|
|
self.houses.push((x, y));
|
|
|
|
|
self.say("A new house was built");
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
/// Sends every animal of `species` its turn and returns the ids of those wanting to breed.
|
|
|
|
|
fn live(
|
|
|
|
|
&self,
|
|
|
|
|
species: Species,
|
|
|
|
|
census: &mut Census,
|
|
|
|
|
turns: Vec<(AnimalId, AnimalTurn)>,
|
|
|
|
|
) -> Vec<AnimalId> {
|
|
|
|
|
let request = self.mailbox.request();
|
|
|
|
|
let mut sent = 0;
|
|
|
|
|
for (id, turn) in turns {
|
|
|
|
|
if let Some(actor) = self.animal_actors(species).get(&id) {
|
|
|
|
|
actor.tell(AnimalMsg::Live(request, turn), self.mailbox.reply_to());
|
|
|
|
|
sent += 1;
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let lived = self
|
|
|
|
|
.mailbox
|
|
|
|
|
.collect(request, sent)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|reply| match reply {
|
|
|
|
|
ReplyBody::Lived(id, animal, wants_to_breed) => (id, (animal, wants_to_breed)),
|
|
|
|
|
other => panic!("animal actor answered its turn with {other:?}"),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
let animals = census.animals_mut(species);
|
|
|
|
|
let mut breeders = Vec::new();
|
|
|
|
|
for (id, (animal, wants_to_breed)) in sorted_by_key(lived) {
|
|
|
|
|
if let Some(entry) = animals.iter_mut().find(|(key, _)| *key == id) {
|
|
|
|
|
entry.1 = animal;
|
|
|
|
|
}
|
|
|
|
|
if wants_to_breed {
|
|
|
|
|
breeders.push(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
breeders
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lets `breeders` give birth, in order, while the species is below `max_population`.
|
|
|
|
|
fn breed(
|
|
|
|
|
&mut self,
|
|
|
|
|
species: Species,
|
|
|
|
|
census: &mut Census,
|
|
|
|
|
breeders: Vec<AnimalId>,
|
|
|
|
|
max_population: usize,
|
|
|
|
|
cost: BirthCost,
|
|
|
|
|
newborn_energy: f32,
|
|
|
|
|
) {
|
|
|
|
|
for id in breeders {
|
|
|
|
|
let living = census
|
|
|
|
|
.animals_mut(species)
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(_, animal)| !animal.dead)
|
|
|
|
|
.count();
|
|
|
|
|
if living >= max_population {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let Some(actor) = self.animal_actors(species).get(&id) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let reply = self.ask(actor, |request| AnimalMsg::GiveBirth(request, cost));
|
|
|
|
|
let ReplyBody::Animal(_, parent) = reply else {
|
|
|
|
|
panic!("animal actor answered a birth with {reply:?}");
|
|
|
|
|
};
|
|
|
|
|
let position = parent.position();
|
|
|
|
|
if let Some(entry) = census
|
|
|
|
|
.animals_mut(species)
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|(key, _)| *key == id)
|
|
|
|
|
{
|
|
|
|
|
entry.1 = parent;
|
|
|
|
|
}
|
|
|
|
|
let newborn = self.spawn_animal(species, position, newborn_energy);
|
|
|
|
|
census.animals_mut(species).push(newborn);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn tick_rabbits(&mut self, census: &mut Census) {
|
|
|
|
|
let mut turns = Vec::with_capacity(census.rabbits.len());
|
|
|
|
|
for index in 0..census.rabbits.len() {
|
|
|
|
|
let (id, rabbit) = census.rabbits[index].clone();
|
|
|
|
|
if rabbit.dead {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let pos = rabbit.position();
|
|
|
|
|
let on_grass = matches!(self.tiles[pos.1 * self.width + pos.0], GRASS | FOREST);
|
|
|
|
|
let wolf =
|
|
|
|
|
Self::nearest_animal(&census.wolves, pos, self.config.rabbit_wolf_detection_range);
|
|
|
|
|
let c = Ctx {
|
|
|
|
|
wolf_near: wolf.is_some(),
|
|
|
|
|
roll: f64::from(self.rng.f()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let position = match (self.decide("rabbit", c).as_str(), wolf) {
|
|
|
|
|
("flee", Some(w)) => {
|
|
|
|
|
let (wx, wy) = census.wolves[w].1.position();
|
|
|
|
|
self.move_dir(pos.0, pos.1, direction(wx, pos.0), direction(wy, pos.1))
|
|
|
|
|
}
|
|
|
|
|
("wander", _) => self.wander(pos.0, pos.1),
|
|
|
|
|
_ => pos,
|
|
|
|
|
};
|
|
|
|
|
let may_breed = self.rng.chance(self.config.rabbit_breed_chance);
|
|
|
|
|
turns.push((
|
|
|
|
|
id,
|
|
|
|
|
AnimalTurn {
|
|
|
|
|
drain: self.config.rabbit_energy_drain,
|
|
|
|
|
gain: on_grass.then_some(self.config.rabbit_energy_regen),
|
|
|
|
|
max_energy: self.config.rabbit_energy_max,
|
|
|
|
|
position,
|
|
|
|
|
breed_above: may_breed.then_some(self.config.rabbit_breed_energy),
|
|
|
|
|
death_threshold: self.config.animal_energy_death_threshold,
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let breeders = self.live(Species::Rabbit, census, turns);
|
|
|
|
|
let cost = BirthCost {
|
|
|
|
|
energy: self.config.rabbit_breed_energy_cost,
|
|
|
|
|
death_threshold: self.config.animal_energy_death_threshold,
|
2026-10-07 16:42:59 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.breed(
|
|
|
|
|
Species::Rabbit,
|
|
|
|
|
census,
|
|
|
|
|
breeders,
|
|
|
|
|
self.config.rabbit_max_population,
|
|
|
|
|
cost,
|
|
|
|
|
self.config.rabbit_newborn_energy,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tick_wolves(&mut self, census: &mut Census) {
|
|
|
|
|
let mut turns = Vec::with_capacity(census.wolves.len());
|
|
|
|
|
for index in 0..census.wolves.len() {
|
|
|
|
|
let (id, wolf) = census.wolves[index].clone();
|
|
|
|
|
if wolf.dead {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let pos = wolf.position();
|
|
|
|
|
let prey = Self::nearest_animal(&census.rabbits, pos, self.config.wolf_prey_range);
|
|
|
|
|
let ctx = Ctx {
|
|
|
|
|
prey_near: prey.is_some(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let mut gain = None;
|
|
|
|
|
let position = if let ("hunt", Some(rabbit)) = (self.decide("wolf", ctx).as_str(), prey)
|
|
|
|
|
{
|
|
|
|
|
let target = census.rabbits[rabbit].1.position();
|
|
|
|
|
if cheb(pos, target) <= self.config.attack_range {
|
|
|
|
|
if self.rng.chance(self.config.wolf_hunt_success_chance)
|
|
|
|
|
&& self.hunt_rabbit(census, rabbit)
|
|
|
|
|
{
|
|
|
|
|
gain = Some(self.config.wolf_hunt_energy_gain);
|
|
|
|
|
}
|
|
|
|
|
pos
|
|
|
|
|
} else {
|
|
|
|
|
self.move_toward(pos, target)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
} else {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.wander(pos.0, pos.1)
|
|
|
|
|
};
|
|
|
|
|
let may_breed = self.rng.chance(self.config.wolf_breed_chance);
|
|
|
|
|
turns.push((
|
|
|
|
|
id,
|
|
|
|
|
AnimalTurn {
|
|
|
|
|
drain: self.config.wolf_energy_drain,
|
|
|
|
|
gain,
|
|
|
|
|
max_energy: self.config.wolf_energy_max,
|
|
|
|
|
position,
|
|
|
|
|
breed_above: may_breed.then_some(self.config.wolf_breed_energy),
|
|
|
|
|
death_threshold: self.config.animal_energy_death_threshold,
|
|
|
|
|
},
|
|
|
|
|
));
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let breeders = self.live(Species::Wolf, census, turns);
|
|
|
|
|
let cost = BirthCost {
|
|
|
|
|
energy: self.config.wolf_breed_energy_cost,
|
|
|
|
|
death_threshold: self.config.animal_energy_death_threshold,
|
|
|
|
|
};
|
|
|
|
|
self.breed(
|
|
|
|
|
Species::Wolf,
|
|
|
|
|
census,
|
|
|
|
|
breeders,
|
|
|
|
|
self.config.wolf_max_population,
|
|
|
|
|
cost,
|
|
|
|
|
self.config.wolf_newborn_energy,
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
/// Replaces the running script. Behaviours and tuning constants apply
|
|
|
|
|
/// immediately; map size and calendar defaults apply on reset.
|
2026-10-07 16:42:59 +00:00
|
|
|
pub fn set_script(&mut self, src: &str) -> Result<(), String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let script = SimScript::load(src)?;
|
|
|
|
|
self.vm = script.vm();
|
|
|
|
|
self.config = script.config.clone();
|
|
|
|
|
self.script = script;
|
2026-10-07 16:42:59 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:58:34 +00:00
|
|
|
pub fn restarted(&self, seed: u64) -> Self {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let mut world = Self::new_with_script(seed, self.script.clone());
|
2026-10-08 06:58:34 +00:00
|
|
|
world.speed = self.speed;
|
|
|
|
|
world.paused = self.paused;
|
2026-10-11 15:38:57 +00:00
|
|
|
world.calendar = self.calendar.clone();
|
|
|
|
|
world.calendar.normalize();
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
world.tick = (world.calendar.ticks_per_day / world.config.starting_tick_fraction).max(1);
|
2026-10-08 06:58:34 +00:00
|
|
|
world
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn regenerated_seed(&self) -> u64 {
|
|
|
|
|
let mut seed = rand::random::<u64>();
|
|
|
|
|
if seed == self.seed {
|
|
|
|
|
seed = seed.wrapping_add(1);
|
|
|
|
|
}
|
|
|
|
|
seed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn seed(&self) -> u64 {
|
|
|
|
|
self.seed
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:44:30 +00:00
|
|
|
pub fn snapshot_json(&self) -> Result<String, serde_json::Error> {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let census = self.census();
|
2026-10-08 06:44:30 +00:00
|
|
|
serde_json::to_string(&WorldSnapshot {
|
|
|
|
|
version: 1,
|
2026-10-08 06:58:34 +00:00
|
|
|
seed: Some(self.seed),
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
width: self.width,
|
|
|
|
|
height: self.height,
|
2026-10-08 06:44:30 +00:00
|
|
|
tiles: self.tiles.clone(),
|
|
|
|
|
res: self.res.clone(),
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
people: census.people,
|
|
|
|
|
rabbits: census.rabbits.into_iter().map(|(_, a)| a).collect(),
|
|
|
|
|
wolves: census.wolves.into_iter().map(|(_, a)| a).collect(),
|
2026-10-08 06:44:30 +00:00
|
|
|
houses: self.houses.clone(),
|
|
|
|
|
home: self.home,
|
|
|
|
|
food: self.food,
|
|
|
|
|
wood: self.wood,
|
|
|
|
|
stone: self.stone,
|
|
|
|
|
tools: self.tools,
|
|
|
|
|
rng: self.rng.0,
|
|
|
|
|
tick: self.tick,
|
|
|
|
|
next_id: self.next_id,
|
|
|
|
|
log: self.log.clone(),
|
|
|
|
|
paused: self.paused,
|
|
|
|
|
speed: self.speed,
|
|
|
|
|
terrain_rev: self.terrain_rev,
|
2026-10-11 15:38:57 +00:00
|
|
|
calendar: self.calendar.clone(),
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
partnerships: self.observe_partnerships(),
|
2026-10-11 15:48:26 +00:00
|
|
|
next_partnership_id: self.next_partnership_id,
|
2026-10-08 06:44:30 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn script(&self) -> &str {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.script.source()
|
2026-10-08 06:44:30 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
#[cfg(test)]
|
2026-10-08 06:58:34 +00:00
|
|
|
pub fn from_snapshot(snapshot: &str, fallback_seed: u64) -> Result<Self, String> {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
Self::from_snapshot_with_script(snapshot, fallback_seed, default_script())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Restores world state from a snapshot; `script` (normally `config.rn`
|
|
|
|
|
/// on disk) supplies the behaviours and tuning constants.
|
|
|
|
|
pub fn from_snapshot_with_script(
|
|
|
|
|
snapshot: &str,
|
|
|
|
|
fallback_seed: u64,
|
|
|
|
|
script: SimScript,
|
|
|
|
|
) -> Result<Self, String> {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let config = &script.config;
|
2026-10-11 16:04:36 +00:00
|
|
|
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"))
|
|
|
|
|
})
|
|
|
|
|
});
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let mut snapshot_value = snapshot_value;
|
|
|
|
|
if snapshot_value.get("calendar").is_none() {
|
|
|
|
|
let calendar = serde_json::to_value(&config.calendar).map_err(|e| e.to_string())?;
|
|
|
|
|
snapshot_value
|
|
|
|
|
.as_object_mut()
|
|
|
|
|
.ok_or("world snapshot must be a JSON object")?
|
|
|
|
|
.insert("calendar".into(), calendar);
|
|
|
|
|
}
|
2026-10-11 16:04:36 +00:00
|
|
|
let saved: WorldSnapshot =
|
|
|
|
|
serde_json::from_value(snapshot_value).map_err(|e| e.to_string())?;
|
2026-10-08 06:44:30 +00:00
|
|
|
if saved.version != 1 {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"unsupported world snapshot version {}",
|
|
|
|
|
saved.version
|
|
|
|
|
));
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
if !(8..=256).contains(&saved.width)
|
|
|
|
|
|| !(8..=256).contains(&saved.height)
|
|
|
|
|
|| saved.tiles.len() != saved.width * saved.height
|
|
|
|
|
|| saved.res.len() != saved.width * saved.height
|
|
|
|
|
{
|
2026-10-08 06:44:30 +00:00
|
|
|
return Err("world snapshot has invalid terrain dimensions".into());
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut world = Self::empty(
|
|
|
|
|
saved.seed.unwrap_or(fallback_seed),
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
script,
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
saved.tiles,
|
|
|
|
|
saved.res,
|
|
|
|
|
);
|
|
|
|
|
world.width = saved.width;
|
|
|
|
|
world.height = saved.height;
|
|
|
|
|
world.next_partnership_id = saved.next_partnership_id;
|
|
|
|
|
world.houses = saved.houses;
|
|
|
|
|
world.home = saved.home;
|
|
|
|
|
world.food = saved.food;
|
|
|
|
|
world.wood = saved.wood;
|
|
|
|
|
world.stone = saved.stone;
|
|
|
|
|
world.tools = saved.tools;
|
|
|
|
|
world.rng = Rng(saved.rng);
|
|
|
|
|
world.tick = saved.tick;
|
|
|
|
|
world.next_id = saved.next_id;
|
|
|
|
|
world.log = saved.log;
|
|
|
|
|
world.paused = saved.paused;
|
|
|
|
|
world.speed = saved.speed;
|
|
|
|
|
world.terrain_rev = saved.terrain_rev;
|
|
|
|
|
world.calendar = saved.calendar;
|
2026-10-11 15:38:57 +00:00
|
|
|
world.calendar.normalize();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut people = saved.people;
|
2026-10-11 16:04:36 +00:00
|
|
|
if legacy_people {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for (index, person) in people.iter_mut().enumerate() {
|
|
|
|
|
let stage = if index < world.config.initial_young_adults {
|
|
|
|
|
LifeStage::YoungAdult
|
|
|
|
|
} else {
|
|
|
|
|
LifeStage::Adult
|
|
|
|
|
};
|
|
|
|
|
world.make_grown_up(person, stage);
|
|
|
|
|
}
|
2026-10-11 16:04:36 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut partnerships = saved.partnerships;
|
|
|
|
|
repair_partnerships(&mut people, &mut partnerships);
|
|
|
|
|
for person in people {
|
|
|
|
|
world.spawn_person_actor(person);
|
|
|
|
|
}
|
|
|
|
|
for rabbit in saved.rabbits {
|
|
|
|
|
world.spawn_animal_actor(Species::Rabbit, rabbit);
|
|
|
|
|
}
|
|
|
|
|
for wolf in saved.wolves {
|
|
|
|
|
world.spawn_animal_actor(Species::Wolf, wolf);
|
|
|
|
|
}
|
|
|
|
|
for partnership in partnerships {
|
|
|
|
|
world.spawn_partnership_actor(partnership);
|
2026-10-11 16:04:36 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
Ok(world)
|
2026-10-11 16:04:36 +00:00
|
|
|
}
|
|
|
|
|
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
fn rect(&self, t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> {
|
2026-10-07 16:42:59 +00:00
|
|
|
let n: Vec<i64> = (i..i + 4).filter_map(|k| t.get(k)?.parse().ok()).collect();
|
|
|
|
|
if n.len() != 4 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
let c = |v: i64, max: usize| usize::try_from(v.max(0)).map_or(max - 1, |v| v.min(max - 1));
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let (x0, x1) = (c(n[0].min(n[2]), self.width), c(n[0].max(n[2]), self.width));
|
|
|
|
|
let (y0, y1) = (
|
|
|
|
|
c(n[1].min(n[3]), self.height),
|
|
|
|
|
c(n[1].max(n[3]), self.height),
|
|
|
|
|
);
|
2026-10-07 16:42:59 +00:00
|
|
|
Some((x0, y0, x1, y1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Applies one whitespace-separated edit command; malformed commands are ignored.
|
|
|
|
|
pub fn apply(&mut self, cmd: &str) {
|
|
|
|
|
let t: Vec<&str> = cmd.split_whitespace().collect();
|
|
|
|
|
match t.first().copied() {
|
2026-10-11 19:23:50 +00:00
|
|
|
Some("pause") => self.paused = arg(&t, 1) != Some(0),
|
|
|
|
|
Some("calendar") => self.edit_calendar(&t),
|
|
|
|
|
Some("stage") => self.edit_stage(&t),
|
|
|
|
|
Some("speed") => {
|
|
|
|
|
if let Some(n) = arg(&t, 1) {
|
|
|
|
|
self.speed = clamp_to(n, 1, i64::from(self.config.max_simulation_speed));
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Some("stock") => self.edit_stock(&t),
|
|
|
|
|
Some("person") => self.edit_person(&t),
|
|
|
|
|
Some("energy") => self.edit_energy(&t),
|
|
|
|
|
Some("terrain") => {
|
|
|
|
|
if let (Some((x0, y0, x1, y1)), Some(kind)) = (self.rect(&t, 1), arg(&t, 5))
|
|
|
|
|
&& let Ok(kind) = u8::try_from(kind)
|
|
|
|
|
&& kind <= 6
|
|
|
|
|
{
|
|
|
|
|
self.paint_terrain(x0, y0, x1, y1, kind);
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
Some("res") => self.edit_resources(&t),
|
|
|
|
|
Some("spawn") => self.edit_spawn(&t),
|
|
|
|
|
Some("house") => self.edit_houses(&t),
|
|
|
|
|
Some("remove") => self.edit_remove(&t),
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_calendar(&mut self, t: &[&str]) {
|
|
|
|
|
if let (Some(setting), Some(value)) = (t.get(1), arg(t, 2)) {
|
|
|
|
|
let days: u32 = clamp_to(value, 1, 100);
|
|
|
|
|
match *setting {
|
|
|
|
|
"ticks_per_day" => self.calendar.ticks_per_day = clamp_to(value, 1, 1_000_000),
|
|
|
|
|
"days_per_week" => self.calendar.days_per_week = days,
|
|
|
|
|
"weeks_per_month" => self.calendar.weeks_per_month = days,
|
|
|
|
|
"months_per_year" => self.calendar.months_per_year = days,
|
|
|
|
|
_ => {}
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_stage(&mut self, t: &[&str]) {
|
|
|
|
|
let (Some(stage), Some(property)) = (t.get(1), t.get(2)) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
if *property == "fertile" {
|
|
|
|
|
if let Some(value) = arg(t, 3)
|
|
|
|
|
&& let Some(range) = self.calendar.duration_mut(stage)
|
|
|
|
|
{
|
|
|
|
|
range.fertile = value != 0;
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
} else if let (Some(min_days), Some(max_days)) = (arg(t, 2), arg(t, 3)) {
|
|
|
|
|
let min_days: u32 = clamp_to(min_days, 1, 1_000_000);
|
|
|
|
|
let max_days: u32 = clamp_to(max_days, 1, 1_000_000);
|
|
|
|
|
if min_days <= max_days
|
|
|
|
|
&& let Some(range) = self.calendar.duration_mut(stage)
|
|
|
|
|
{
|
|
|
|
|
range.min_days = min_days;
|
|
|
|
|
range.max_days = max_days;
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_stock(&mut self, t: &[&str]) {
|
|
|
|
|
if let (Some(name), Some(v)) = (t.get(1), arg(t, 2)) {
|
|
|
|
|
let v = clamp_to(v, 0, i64::from(self.config.max_stock_edit));
|
|
|
|
|
match *name {
|
|
|
|
|
"food" => self.food = v,
|
|
|
|
|
"wood" => self.wood = v,
|
|
|
|
|
"stone" => self.stone = v,
|
|
|
|
|
"tools" => self.tools = v,
|
|
|
|
|
_ => {}
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_person(&mut self, t: &[&str]) {
|
|
|
|
|
let max_inventory = i64::from(self.config.max_person_inventory_edit);
|
|
|
|
|
let max_hunger = self.config.max_hunger_edit;
|
|
|
|
|
if let (Some(id), Some(field), Some(v)) = (arg(t, 1), t.get(2), arg(t, 3))
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
&& let Ok(id) = u32::try_from(id)
|
2026-10-11 19:23:50 +00:00
|
|
|
{
|
|
|
|
|
let u = clamp_to(v, 0, max_inventory);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let edit = match *field {
|
|
|
|
|
"hunger" => PersonEdit::Hunger(small_f32(v.clamp(0, max_hunger))),
|
|
|
|
|
"food" => PersonEdit::Food(u),
|
|
|
|
|
"wood" => PersonEdit::Wood(u),
|
|
|
|
|
"stone" => PersonEdit::Stone(u),
|
|
|
|
|
_ => return,
|
|
|
|
|
};
|
|
|
|
|
self.tell_person(id, PersonMsg::Edit(edit));
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_energy(&mut self, t: &[&str]) {
|
|
|
|
|
if let (Some((x0, y0, x1, y1)), Some(v)) = (self.rect(t, 1), arg(t, 5)) {
|
|
|
|
|
let max = self.config.max_animal_energy_edit.trunc().max(1.0);
|
|
|
|
|
let v = small_f32(v.clamp(1, 1_000_000)).min(max);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let area = Area { x0, y0, x1, y1 };
|
|
|
|
|
for actor in self.rabbit_actors.values().chain(self.wolf_actors.values()) {
|
|
|
|
|
actor.tell(AnimalMsg::SetEnergyWithin(area, v), None);
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_resources(&mut self, t: &[&str]) {
|
|
|
|
|
let (Some((x0, y0, x1, y1)), Some(kind), Some(amt)) =
|
|
|
|
|
(self.rect(t, 1), arg(t, 5), arg(t, 6))
|
|
|
|
|
else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Ok(kind) = u8::try_from(kind) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
if kind > 3 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let amt = clamp_to(amt, 1, i64::from(self.config.max_resource_edit));
|
|
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
|
|
|
|
let i = y * self.width + x;
|
|
|
|
|
let free =
|
|
|
|
|
self.walkable((x, y)) && (x, y) != self.home && !self.houses.contains(&(x, y));
|
|
|
|
|
if kind == 0 {
|
|
|
|
|
self.res[i] = (0, 0);
|
|
|
|
|
} else if free {
|
|
|
|
|
self.res[i] = (kind, amt);
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_spawn(&mut self, t: &[&str]) {
|
|
|
|
|
let (Some(what), Some((x0, y0, x1, y1))) = (t.get(1), self.rect(t, 2)) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let mut left = self.config.manual_spawn_limit;
|
|
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
|
|
|
|
if left == 0 || !self.walkable((x, y)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
left -= 1;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let energy = self.config.initial_animal_energy;
|
2026-10-11 19:23:50 +00:00
|
|
|
match *what {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
"person" => {
|
|
|
|
|
self.spawn_person_at((x, y));
|
|
|
|
|
}
|
|
|
|
|
"rabbit" => {
|
|
|
|
|
self.spawn_animal(Species::Rabbit, (x, y), energy);
|
|
|
|
|
}
|
|
|
|
|
"wolf" => {
|
|
|
|
|
self.spawn_animal(Species::Wolf, (x, y), energy);
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
_ => left += 1,
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_houses(&mut self, t: &[&str]) {
|
|
|
|
|
let Some((x0, y0, x1, y1)) = self.rect(t, 1) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
|
|
|
|
if self.houses.len() < self.config.max_houses
|
|
|
|
|
&& self.walkable((x, y))
|
|
|
|
|
&& self.res[y * self.width + x].0 == 0
|
|
|
|
|
&& (x, y) != self.home
|
|
|
|
|
&& !self.houses.contains(&(x, y))
|
|
|
|
|
{
|
|
|
|
|
self.houses.push((x, y));
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_remove(&mut self, t: &[&str]) {
|
|
|
|
|
let (Some((x0, y0, x1, y1)), Some(what)) = (self.rect(t, 1), t.get(5)) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let area = Area { x0, y0, x1, y1 };
|
2026-10-11 19:23:50 +00:00
|
|
|
let all = *what == "all";
|
|
|
|
|
if all || *what == "people" {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut people = self.observe_people();
|
|
|
|
|
let removed: Vec<u32> = people
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|p| area.contains(p.position()))
|
|
|
|
|
.map(|p| p.id)
|
|
|
|
|
.collect();
|
|
|
|
|
if !removed.is_empty() {
|
|
|
|
|
for &id in &removed {
|
|
|
|
|
self.retire_person(id);
|
|
|
|
|
}
|
|
|
|
|
people.retain(|p| !removed.contains(&p.id));
|
|
|
|
|
self.prune_partnerships(&mut people);
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for (name, species) in [("rabbits", Species::Rabbit), ("wolves", Species::Wolf)] {
|
|
|
|
|
if all || *what == name {
|
|
|
|
|
for (id, animal) in self.observe_animals(species) {
|
|
|
|
|
if area.contains(animal.position()) {
|
|
|
|
|
self.retire_animal(species, id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
if all || *what == "houses" {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
self.houses.retain(|h| !area.contains(*h));
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
if all || *what == "res" {
|
|
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
|
|
|
|
self.res[y * self.width + x] = (0, 0);
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn paint_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, kind: u8) {
|
|
|
|
|
let land = matches!(kind, SAND | GRASS | FOREST | HILLS);
|
|
|
|
|
for y in y0..=y1 {
|
|
|
|
|
for x in x0..=x1 {
|
|
|
|
|
if !land && (x, y) == self.home {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.tiles[y * self.width + x] = kind;
|
2026-10-07 16:42:59 +00:00
|
|
|
if !land {
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
self.res[y * self.width + x] = (0, 0);
|
2026-10-07 16:42:59 +00:00
|
|
|
self.houses.retain(|h| *h != (x, y));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !land {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
for species in [Species::Rabbit, Species::Wolf] {
|
|
|
|
|
for (id, animal) in self.observe_animals(species) {
|
|
|
|
|
if !self.walkable(animal.position()) {
|
|
|
|
|
self.retire_animal(species, id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for person in self.observe_people() {
|
|
|
|
|
if !self.walkable(person.position()) {
|
|
|
|
|
self.tell_person(person.id, PersonMsg::MoveTo(self.home));
|
|
|
|
|
}
|
2026-10-07 16:42:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.terrain_rev += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 16:04:37 +00:00
|
|
|
pub fn terrain_json(&self) -> String {
|
|
|
|
|
let tiles: String = self.tiles.iter().map(|t| (b'0' + t) as char).collect();
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
format!(
|
|
|
|
|
"{{\"w\":{},\"h\":{},\"tiles\":\"{}\"}}",
|
|
|
|
|
self.width, self.height, tiles
|
|
|
|
|
)
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 19:23:50 +00:00
|
|
|
fn write_calendar_json(&self, s: &mut String) {
|
2026-10-11 15:38:57 +00:00
|
|
|
let _ = write!(
|
2026-10-11 19:23:50 +00:00
|
|
|
*s,
|
2026-10-11 15:38:57 +00:00
|
|
|
"\"calendar\":{{\"ticks_per_day\":{},\"days_per_week\":{},\"weeks_per_month\":{},\"months_per_year\":{},",
|
|
|
|
|
self.calendar.ticks_per_day,
|
|
|
|
|
self.calendar.days_per_week,
|
|
|
|
|
self.calendar.weeks_per_month,
|
|
|
|
|
self.calendar.months_per_year
|
|
|
|
|
);
|
|
|
|
|
for (index, (name, range)) in [
|
|
|
|
|
("toddler", self.calendar.toddler),
|
|
|
|
|
("child", self.calendar.child),
|
|
|
|
|
("teen", self.calendar.teen),
|
|
|
|
|
("young_adult", self.calendar.young_adult),
|
|
|
|
|
("adult", self.calendar.adult),
|
|
|
|
|
("older_adult", self.calendar.older_adult),
|
|
|
|
|
("elderly", self.calendar.elderly),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
|
|
|
|
if index > 0 {
|
|
|
|
|
s.push(',');
|
|
|
|
|
}
|
|
|
|
|
let _ = write!(
|
2026-10-11 19:23:50 +00:00
|
|
|
*s,
|
2026-10-11 15:48:26 +00:00
|
|
|
"\"{name}\":{{\"min_days\":{},\"max_days\":{},\"fertile\":{}}}",
|
|
|
|
|
range.min_days, range.max_days, range.fertile
|
2026-10-11 15:38:57 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
s.push_str("},");
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write_entities_json(&self, s: &mut String) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let census = self.census();
|
2026-10-07 16:04:37 +00:00
|
|
|
s.push_str("\"houses\":[");
|
|
|
|
|
let j: Vec<String> = self
|
|
|
|
|
.houses
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|h| format!("[{},{}]", h.0, h.1))
|
|
|
|
|
.collect();
|
|
|
|
|
s.push_str(&j.join(","));
|
|
|
|
|
s.push_str("],\"people\":[");
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let j: Vec<String> = census
|
2026-10-07 16:04:37 +00:00
|
|
|
.people
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|p| {
|
|
|
|
|
format!(
|
2026-10-11 15:48:26 +00:00
|
|
|
"[{},{},{},\"{}\",{:.0},{},{},{},\"{}\",{},{}]",
|
|
|
|
|
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())
|
2026-10-07 16:04:37 +00:00
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
s.push_str(&j.join(","));
|
|
|
|
|
s.push_str("],\"rabbits\":[");
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let j: Vec<String> = census
|
2026-10-07 16:04:37 +00:00
|
|
|
.rabbits
|
|
|
|
|
.iter()
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
.map(|(_, a)| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
|
2026-10-07 16:04:37 +00:00
|
|
|
.collect();
|
|
|
|
|
s.push_str(&j.join(","));
|
|
|
|
|
s.push_str("],\"wolves\":[");
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let j: Vec<String> = census
|
2026-10-07 16:04:37 +00:00
|
|
|
.wolves
|
|
|
|
|
.iter()
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
.map(|(_, a)| format!("[{},{},{:.0}]", a.x, a.y, a.energy))
|
2026-10-07 16:04:37 +00:00
|
|
|
.collect();
|
|
|
|
|
s.push_str(&j.join(","));
|
|
|
|
|
s.push_str("],\"res\":[");
|
|
|
|
|
let mut first = true;
|
|
|
|
|
for (i, r) in self.res.iter().enumerate() {
|
|
|
|
|
if r.0 != 0 {
|
|
|
|
|
if !first {
|
|
|
|
|
s.push(',');
|
|
|
|
|
}
|
|
|
|
|
first = false;
|
2026-10-11 19:23:50 +00:00
|
|
|
let _ = write!(
|
|
|
|
|
*s,
|
|
|
|
|
"[{},{},{},{}]",
|
|
|
|
|
i % self.width,
|
|
|
|
|
i / self.width,
|
|
|
|
|
r.0,
|
|
|
|
|
r.1
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s.push_str("],\"log\":[");
|
2026-10-11 19:23:50 +00:00
|
|
|
let j: Vec<String> = self.log.iter().map(|l| format!("\"{l}\"")).collect();
|
2026-10-07 16:04:37 +00:00
|
|
|
s.push_str(&j.join(","));
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn state_json(&self) -> String {
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
let days_per_month =
|
|
|
|
|
u64::from(self.calendar.days_per_week) * u64::from(self.calendar.weeks_per_month);
|
|
|
|
|
let days_per_year = days_per_month * u64::from(self.calendar.months_per_year);
|
|
|
|
|
let day = self.tick / self.calendar.ticks_per_day;
|
|
|
|
|
let year = day / days_per_year + 1;
|
|
|
|
|
let month = (day / days_per_month) % u64::from(self.calendar.months_per_year) + 1;
|
|
|
|
|
let week = (day / u64::from(self.calendar.days_per_week))
|
|
|
|
|
% u64::from(self.calendar.weeks_per_month)
|
|
|
|
|
+ 1;
|
|
|
|
|
let date_day = day % u64::from(self.calendar.days_per_week) + 1;
|
|
|
|
|
let actor_stats = self.actor_system.stats();
|
|
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"limits\":{{\"speed\":{},\"stock\":{},\"person_inventory\":{},\"hunger\":{},\"animal_energy\":{},\"resource\":{},\"spawn\":{}}},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
|
|
|
|
|
self.paused,
|
|
|
|
|
self.speed,
|
|
|
|
|
self.seed,
|
|
|
|
|
self.terrain_rev,
|
|
|
|
|
self.tick,
|
|
|
|
|
day + 1,
|
|
|
|
|
year,
|
|
|
|
|
month,
|
|
|
|
|
week,
|
|
|
|
|
date_day,
|
|
|
|
|
self.day_fraction() * 24.0,
|
|
|
|
|
self.daylight(),
|
|
|
|
|
self.config.max_simulation_speed,
|
|
|
|
|
self.config.max_stock_edit,
|
|
|
|
|
self.config.max_person_inventory_edit,
|
|
|
|
|
self.config.max_hunger_edit,
|
|
|
|
|
self.config.max_animal_energy_edit,
|
|
|
|
|
self.config.max_resource_edit,
|
|
|
|
|
self.config.manual_spawn_limit,
|
|
|
|
|
self.food,
|
|
|
|
|
self.wood,
|
|
|
|
|
self.stone,
|
|
|
|
|
self.tools,
|
|
|
|
|
self.home.0,
|
|
|
|
|
self.home.1
|
|
|
|
|
);
|
|
|
|
|
self.write_calendar_json(&mut s);
|
|
|
|
|
self.write_entities_json(&mut s);
|
2026-10-11 16:58:07 +00:00
|
|
|
let _ = write!(
|
|
|
|
|
s,
|
|
|
|
|
"],\"actor_stats\":{{\"actors_created\":{},\"actors_terminated\":{},\"actors_restarted\":{},\"messages_sent\":{},\"messages_failed\":{},\"system_messages_sent\":{},\"live_actors\":{}}}}}",
|
|
|
|
|
actor_stats.actors_created,
|
|
|
|
|
actor_stats.actors_terminated,
|
|
|
|
|
actor_stats.actors_restarted,
|
|
|
|
|
actor_stats.messages_sent,
|
|
|
|
|
actor_stats.messages_failed,
|
|
|
|
|
actor_stats.system_messages_sent,
|
|
|
|
|
actor_stats.live_actors()
|
|
|
|
|
);
|
2026-10-07 16:04:37 +00:00
|
|
|
s
|
|
|
|
|
}
|
2026-10-12 10:43:05 +00:00
|
|
|
|
|
|
|
|
#[cfg(feature = "console")]
|
|
|
|
|
pub fn handle_picard_console_text(&self, request: &str) -> String {
|
|
|
|
|
self.actor_system.console().handle_text(request)
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
}
|
2026-10-08 06:44:30 +00:00
|
|
|
|
2026-10-11 15:29:17 +00:00
|
|
|
impl Drop for World {
|
|
|
|
|
fn drop(&mut self) {
|
2026-10-11 18:42:29 +00:00
|
|
|
// Shutdown starts eagerly; Drop must not block waiting for completion.
|
|
|
|
|
drop(self.actor_system.shutdown());
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:44:30 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
use super::actors::{Activity, AnimalTurn, Goods, PersonMsg, ReplyBody};
|
|
|
|
|
use super::{Animal, DEFAULT_SCRIPT, H, LifeStage, SimScript, W, World, cheb, default_config};
|
2026-10-11 15:48:26 +00:00
|
|
|
|
2026-10-12 14:25:51 +00:00
|
|
|
/// 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::<Vec<_>>()
|
|
|
|
|
.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_fertile(world: &World, stage: LifeStage) -> bool {
|
|
|
|
|
world
|
|
|
|
|
.calendar
|
|
|
|
|
.duration(stage.key())
|
|
|
|
|
.is_some_and(|duration| duration.fertile)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:58:07 +00:00
|
|
|
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");
|
|
|
|
|
state
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:48:26 +00:00
|
|
|
#[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;
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let id = world.spawn_person_at(world.home).id;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
world.observe_people().last().unwrap().stage,
|
|
|
|
|
LifeStage::Toddler
|
|
|
|
|
);
|
2026-10-11 15:48:26 +00:00
|
|
|
|
|
|
|
|
for expected in [
|
|
|
|
|
LifeStage::Child,
|
|
|
|
|
LifeStage::Teen,
|
|
|
|
|
LifeStage::YoungAdult,
|
|
|
|
|
LifeStage::Adult,
|
|
|
|
|
LifeStage::OlderAdult,
|
|
|
|
|
LifeStage::Elderly,
|
|
|
|
|
LifeStage::Dead,
|
|
|
|
|
] {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let people = world.advance_people();
|
|
|
|
|
let person = people.iter().find(|person| person.id == id).unwrap();
|
|
|
|
|
assert_eq!(person.stage, expected);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let people = world.observe_people();
|
|
|
|
|
assert!(people.iter().find(|person| person.id == id).unwrap().dead);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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.food = 1000;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut census = world.census();
|
|
|
|
|
let eligible = census
|
2026-10-12 14:25:51 +00:00
|
|
|
.people
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|person| person.stage.can_partner())
|
|
|
|
|
.count();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
world.find_partners(&mut census.people);
|
2026-10-12 14:25:51 +00:00
|
|
|
let partnerships = eligible / 2;
|
|
|
|
|
assert!(partnerships > 0);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert_eq!(world.partnership_actors.len(), partnerships);
|
|
|
|
|
assert_eq!(world.observe_partnerships().len(), partnerships);
|
2026-10-11 15:48:26 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
world
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
.observe_people()
|
2026-10-11 15:48:26 +00:00
|
|
|
.iter()
|
|
|
|
|
.filter(|person| person.partner_id.is_some())
|
|
|
|
|
.count(),
|
2026-10-12 14:25:51 +00:00
|
|
|
partnerships * 2
|
2026-10-11 15:48:26 +00:00
|
|
|
);
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let initial_population = census.people.len();
|
2026-10-12 14:25:51 +00:00
|
|
|
let births = 3;
|
|
|
|
|
world.config.population_capacity = initial_population + births;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
while census.people.len() < world.population_capacity() {
|
2026-10-11 15:48:26 +00:00
|
|
|
world.tick += 1;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
world.have_children(1, &mut census);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let people = world.observe_people();
|
|
|
|
|
assert_eq!(people.len(), world.population_capacity());
|
|
|
|
|
assert_eq!(people.len() - initial_population, births);
|
|
|
|
|
let toddler = people
|
2026-10-11 15:48:26 +00:00
|
|
|
.iter()
|
|
|
|
|
.find(|person| person.stage == LifeStage::Toddler)
|
|
|
|
|
.expect("fertile partner actors should eventually have a child");
|
|
|
|
|
assert_eq!(toddler.age_ticks, 0);
|
2026-10-11 16:04:36 +00:00
|
|
|
assert_eq!((toddler.x, toddler.y), world.home);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:13:10 +00:00
|
|
|
#[test]
|
|
|
|
|
fn each_fertile_partnership_can_have_a_weekly_birth() {
|
|
|
|
|
let mut world = World::new(42);
|
|
|
|
|
world.calendar.ticks_per_day = 1;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut census = world.census();
|
|
|
|
|
world.find_partners(&mut census.people);
|
|
|
|
|
let partnerships = world.observe_partnerships();
|
|
|
|
|
let fertile_partnerships = partnerships
|
2026-10-12 14:25:51 +00:00
|
|
|
.iter()
|
|
|
|
|
.filter(|partnership| {
|
|
|
|
|
[partnership.first_person_id, partnership.second_person_id]
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|id| {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let person = census.people.iter().find(|person| person.id == *id);
|
2026-10-12 14:25:51 +00:00
|
|
|
person.is_some_and(|person| is_fertile(&world, person.stage))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.count();
|
|
|
|
|
assert!(fertile_partnerships > 0);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let initial_population = census.people.len();
|
|
|
|
|
world.config.population_capacity = initial_population + partnerships.len();
|
2026-10-12 14:25:51 +00:00
|
|
|
world.food = world.config.birth_food_threshold
|
|
|
|
|
+ world.config.birth_food_cost * u32::try_from(fertile_partnerships).unwrap();
|
2026-10-11 16:13:10 +00:00
|
|
|
world.tick += 7;
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
world.have_children(7, &mut census);
|
2026-10-11 16:13:10 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let people = world.observe_people();
|
|
|
|
|
assert_eq!(people.len(), initial_population + fertile_partnerships);
|
2026-10-12 14:25:51 +00:00
|
|
|
assert_eq!(
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
people
|
2026-10-11 16:13:10 +00:00
|
|
|
.iter()
|
|
|
|
|
.filter(|person| person.stage == LifeStage::Toddler)
|
|
|
|
|
.count(),
|
2026-10-12 14:25:51 +00:00
|
|
|
fertile_partnerships
|
2026-10-11 16:13:10 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:39:50 +00:00
|
|
|
#[test]
|
|
|
|
|
fn people_can_pathfind_back_home_around_blocked_terrain() {
|
|
|
|
|
let mut world = World::new(42);
|
|
|
|
|
let start = (59, 48);
|
|
|
|
|
let mut position = start;
|
|
|
|
|
|
|
|
|
|
for _ in 0..W.max(H) * 2 {
|
|
|
|
|
if position == world.home {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
position = world.move_toward(position, world.home);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(position, world.home);
|
2026-10-11 19:23:50 +00:00
|
|
|
assert!(world.walkable(start));
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn village_survives_until_its_first_weekly_birth_check() {
|
|
|
|
|
let mut world = World::new(42);
|
2026-10-11 19:23:50 +00:00
|
|
|
let first_week_tick =
|
|
|
|
|
world.calendar.ticks_per_day * u64::from(world.calendar.days_per_week);
|
2026-10-11 16:39:50 +00:00
|
|
|
let mut daily = Vec::new();
|
|
|
|
|
|
|
|
|
|
while world.tick < first_week_tick {
|
|
|
|
|
world.step();
|
|
|
|
|
if world.tick.is_multiple_of(world.calendar.ticks_per_day) {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
daily.push((world.tick, world.person_actors.len(), world.food));
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 14:25:51 +00:00
|
|
|
let initial_population = world.config.initial_population;
|
|
|
|
|
let minimum_population = if initial_population < world.population_capacity() {
|
|
|
|
|
initial_population + 1
|
|
|
|
|
} else {
|
|
|
|
|
initial_population
|
|
|
|
|
};
|
2026-10-11 16:39:50 +00:00
|
|
|
assert!(
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
world.person_actors.len() >= minimum_population,
|
2026-10-11 16:39:50 +00:00
|
|
|
"village failed to replace its initial population: daily={daily:?}, people={:?}, events={:?}",
|
|
|
|
|
world
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
.observe_people()
|
2026-10-11 16:39:50 +00:00
|
|
|
.iter()
|
|
|
|
|
.map(|person| (person.id, person.hunger, person.action.as_str()))
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
world.log,
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
world.log.iter().all(|event| !event.contains("starved")),
|
|
|
|
|
"people starved before the first birth check: {:?}",
|
|
|
|
|
world.log
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:04:36 +00:00
|
|
|
#[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();
|
|
|
|
|
|
2026-10-12 14:25:51 +00:00
|
|
|
let config = default_config();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let people = restored.observe_people();
|
|
|
|
|
assert_eq!(people.len(), config.initial_population);
|
|
|
|
|
assert_eq!(people[0].stage, LifeStage::YoungAdult);
|
2026-10-11 16:04:36 +00:00
|
|
|
assert!(
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
people
|
2026-10-11 16:04:36 +00:00
|
|
|
.iter()
|
2026-10-12 14:25:51 +00:00
|
|
|
.skip(config.initial_young_adults)
|
2026-10-11 16:04:36 +00:00
|
|
|
.all(|person| person.stage == LifeStage::Adult)
|
|
|
|
|
);
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn partnership_and_life_stage_are_in_persisted_actor_state() {
|
|
|
|
|
let mut world = World::new(42);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut census = world.census();
|
|
|
|
|
world.find_partners(&mut census.people);
|
2026-10-11 15:48:26 +00:00
|
|
|
let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap();
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert!(!world.partnership_actors.is_empty());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
restored.observe_partnerships().len(),
|
|
|
|
|
world.partnership_actors.len()
|
|
|
|
|
);
|
2026-10-11 16:58:07 +00:00
|
|
|
assert_eq!(simulation_state(&restored), simulation_state(&world));
|
2026-10-11 15:48:26 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
serde_json::from_str::<serde_json::Value>(&restored.state_json()).unwrap()["people"][0]
|
|
|
|
|
[8],
|
|
|
|
|
"young_adult"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-08 06:44:30 +00:00
|
|
|
|
2026-10-11 16:04:36 +00:00
|
|
|
#[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));
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let id = world.spawn_person_at(position).id;
|
2026-10-11 16:04:36 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let mut census = world.census();
|
|
|
|
|
world.tick_people(&mut census);
|
2026-10-11 16:04:36 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let child = census.people.iter().find(|person| person.id == id).unwrap();
|
2026-10-11 16:04:36 +00:00
|
|
|
assert!(cheb((child.x, child.y), home) < cheb(position, home));
|
|
|
|
|
assert_eq!(child.action, "growing up");
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let observed = world.observe_people();
|
|
|
|
|
let observed = observed.iter().find(|person| person.id == id).unwrap();
|
|
|
|
|
assert_eq!((observed.x, observed.y), (child.x, child.y));
|
2026-10-11 16:04:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 16:39:50 +00:00
|
|
|
#[test]
|
|
|
|
|
fn a_person_eats_available_food_before_starving() {
|
|
|
|
|
let mut world = World::new(42);
|
|
|
|
|
world.food = 1;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let id = world.observe_people()[0].id;
|
|
|
|
|
world.apply(&format!("person {id} hunger 99"));
|
2026-10-11 16:39:50 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let person = world.observe_people()[0].clone();
|
|
|
|
|
let fed = world.meet_needs(&person).unwrap();
|
2026-10-11 16:39:50 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert!(!fed.dead);
|
|
|
|
|
assert!(fed.hunger < 100.0);
|
2026-10-11 16:39:50 +00:00
|
|
|
assert_eq!(world.food, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:44:30 +00:00
|
|
|
#[test]
|
|
|
|
|
fn snapshot_restores_state_and_rng() {
|
|
|
|
|
let mut original = World::new(42);
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
original.step();
|
|
|
|
|
}
|
|
|
|
|
original.paused = true;
|
|
|
|
|
original.speed = 4;
|
|
|
|
|
|
|
|
|
|
let snapshot = original.snapshot_json().unwrap();
|
2026-10-08 06:58:34 +00:00
|
|
|
let mut restored = World::from_snapshot(&snapshot, 42).unwrap();
|
2026-10-11 16:58:07 +00:00
|
|
|
assert_eq!(simulation_state(&original), simulation_state(&restored));
|
2026-10-08 06:44:30 +00:00
|
|
|
|
|
|
|
|
original.step();
|
|
|
|
|
restored.step();
|
2026-10-11 16:58:07 +00:00
|
|
|
assert_eq!(simulation_state(&original), simulation_state(&restored));
|
2026-10-08 06:44:30 +00:00
|
|
|
}
|
2026-10-08 06:58:34 +00:00
|
|
|
|
|
|
|
|
#[test]
|
2026-10-12 14:25:51 +00:00
|
|
|
fn reset_keeps_seed_and_settings_with_initial_population() {
|
2026-10-08 06:58:34 +00:00
|
|
|
let mut original = World::new(42);
|
|
|
|
|
original.apply("speed 4");
|
|
|
|
|
original.paused = true;
|
|
|
|
|
original.set_script(DEFAULT_SCRIPT).unwrap();
|
|
|
|
|
original.step();
|
|
|
|
|
|
|
|
|
|
let reset = original.restarted(original.seed());
|
|
|
|
|
assert_eq!(reset.seed(), 42);
|
|
|
|
|
assert_eq!(reset.speed, 4);
|
|
|
|
|
assert!(reset.paused);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
reset.person_actors.len(),
|
|
|
|
|
default_config().initial_population
|
|
|
|
|
);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
reset.tick,
|
|
|
|
|
default_config().calendar.ticks_per_day / default_config().starting_tick_fraction
|
|
|
|
|
);
|
2026-10-08 06:58:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn regenerated_seed_differs_from_current_seed() {
|
|
|
|
|
let world = World::new(42);
|
|
|
|
|
assert_ne!(world.regenerated_seed(), world.seed());
|
|
|
|
|
}
|
2026-10-11 15:29:17 +00:00
|
|
|
|
2026-10-12 10:43:05 +00:00
|
|
|
#[test]
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
fn entity_actor_messages_round_trip_through_serde() {
|
|
|
|
|
let message = PersonMsg::Perform(
|
|
|
|
|
7,
|
|
|
|
|
Activity {
|
|
|
|
|
action: "foraging".into(),
|
|
|
|
|
position: (3, 4),
|
|
|
|
|
gathered: Goods {
|
|
|
|
|
food: 2,
|
|
|
|
|
wood: 0,
|
|
|
|
|
stone: 1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-10-12 10:43:05 +00:00
|
|
|
let serialized = serde_json::to_string(&message).unwrap();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let PersonMsg::Perform(request, activity) = serde_json::from_str(&serialized).unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!("serialized person message changed variants");
|
2026-10-12 10:43:05 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert_eq!((request, activity.position), (7, (3, 4)));
|
|
|
|
|
assert_eq!(activity.gathered.stone, 1);
|
|
|
|
|
|
|
|
|
|
let reply = ReplyBody::Lived(
|
|
|
|
|
9,
|
|
|
|
|
Animal {
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 2,
|
|
|
|
|
energy: 30.0,
|
|
|
|
|
dead: false,
|
|
|
|
|
},
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
let serialized = serde_json::to_string(&reply).unwrap();
|
|
|
|
|
let ReplyBody::Lived(id, animal, wants_to_breed) =
|
|
|
|
|
serde_json::from_str(&serialized).unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!("serialized animal reply changed variants");
|
2026-10-12 10:43:05 +00:00
|
|
|
};
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert_eq!((id, animal.position(), wants_to_breed), (9, (1, 2), true));
|
|
|
|
|
let turn: AnimalTurn = serde_json::from_str(
|
|
|
|
|
r#"{"drain":1.0,"gain":null,"max_energy":2.0,"position":[0,0],"breed_above":null,"death_threshold":0.0}"#,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(turn.gain.is_none());
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:29:17 +00:00
|
|
|
#[test]
|
|
|
|
|
fn picard_actors_own_living_entity_state() {
|
|
|
|
|
let mut world = World::new(42);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
let person_id = world.observe_people()[0].id;
|
2026-10-11 15:29:17 +00:00
|
|
|
|
|
|
|
|
world.apply(&format!("person {person_id} hunger 73"));
|
|
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert!((world.observe_people()[0].hunger - 73.0).abs() < f32::EPSILON);
|
|
|
|
|
let entities = world.person_actors.len()
|
|
|
|
|
+ world.rabbit_actors.len()
|
|
|
|
|
+ world.wolf_actors.len()
|
|
|
|
|
+ world.partnership_actors.len();
|
|
|
|
|
let census = world.census();
|
2026-10-11 15:29:17 +00:00
|
|
|
assert_eq!(
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
census.people.len() + census.rabbits.len() + census.wolves.len(),
|
|
|
|
|
entities
|
2026-10-11 15:29:17 +00:00
|
|
|
);
|
2026-10-11 16:58:07 +00:00
|
|
|
let state: serde_json::Value = serde_json::from_str(&world.state_json()).unwrap();
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert!(state["actor_stats"]["live_actors"].as_u64().unwrap() > entities as u64);
|
2026-10-11 16:58:07 +00:00
|
|
|
assert!(state["actor_stats"]["messages_sent"].as_u64().unwrap() > 0);
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
2026-10-11 15:38:57 +00:00
|
|
|
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
#[test]
|
|
|
|
|
fn dead_entities_stop_their_actors_and_dissolve_partnerships() {
|
|
|
|
|
let mut world = World::new(42);
|
|
|
|
|
let mut census = world.census();
|
|
|
|
|
world.find_partners(&mut census.people);
|
|
|
|
|
let partnership = world.observe_partnerships()[0].clone();
|
|
|
|
|
let partner = census
|
|
|
|
|
.people
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|person| person.id == partnership.first_person_id)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
let survivor_id = partnership.second_person_id;
|
|
|
|
|
|
|
|
|
|
world.apply(&format!(
|
|
|
|
|
"remove {x} {y} {x} {y} people",
|
|
|
|
|
x = partner.x,
|
|
|
|
|
y = partner.y
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
assert!(!world.person_actors.contains_key(&partner.id));
|
|
|
|
|
assert!(!world.partnership_actors.contains_key(&partnership.id));
|
|
|
|
|
if let Some(survivor) = world
|
|
|
|
|
.observe_people()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|person| person.id == survivor_id)
|
|
|
|
|
{
|
|
|
|
|
assert_eq!(survivor.partner_id, None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let rabbit = world.observe_animals(super::Species::Rabbit)[0].clone();
|
|
|
|
|
world.apply(&format!(
|
|
|
|
|
"remove {x} {y} {x} {y} rabbits",
|
|
|
|
|
x = rabbit.1.x,
|
|
|
|
|
y = rabbit.1.y
|
|
|
|
|
));
|
|
|
|
|
assert!(!world.rabbit_actors.contains_key(&rabbit.0));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:38:57 +00:00
|
|
|
#[test]
|
|
|
|
|
fn calendar_and_life_stage_ranges_persist_in_snapshots() {
|
|
|
|
|
let mut world = World::new(42);
|
|
|
|
|
world.apply("calendar ticks_per_day 240");
|
|
|
|
|
world.apply("calendar days_per_week 5");
|
|
|
|
|
world.apply("stage toddler 3 9");
|
|
|
|
|
|
|
|
|
|
let snapshot = world.snapshot_json().unwrap();
|
|
|
|
|
let restored = World::from_snapshot(&snapshot, 42).unwrap();
|
|
|
|
|
assert_eq!(restored.calendar.ticks_per_day, 240);
|
|
|
|
|
assert_eq!(restored.calendar.days_per_week, 5);
|
|
|
|
|
assert_eq!(restored.calendar.toddler.min_days, 3);
|
|
|
|
|
assert_eq!(restored.calendar.toddler.max_days, 9);
|
2026-10-11 16:58:07 +00:00
|
|
|
assert_eq!(simulation_state(&world), simulation_state(&restored));
|
2026-10-11 15:38:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshots_without_calendar_use_default_settings() {
|
|
|
|
|
let world = World::new(42);
|
|
|
|
|
let mut snapshot: serde_json::Value =
|
|
|
|
|
serde_json::from_str(&world.snapshot_json().unwrap()).unwrap();
|
|
|
|
|
snapshot.as_object_mut().unwrap().remove("calendar");
|
|
|
|
|
|
|
|
|
|
let restored = World::from_snapshot(&snapshot.to_string(), 42).unwrap();
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
restored.calendar.ticks_per_day,
|
|
|
|
|
default_config().calendar.ticks_per_day
|
|
|
|
|
);
|
2026-10-11 15:38:57 +00:00
|
|
|
assert_eq!(restored.calendar.days_per_week, 7);
|
|
|
|
|
}
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn rune_config_controls_initial_world_and_survives_snapshot_restore() {
|
2026-10-12 14:25:51 +00:00
|
|
|
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)
|
|
|
|
|
});
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
let script = SimScript::load(&source).unwrap();
|
|
|
|
|
let world = World::new_with_script(42, script.clone());
|
|
|
|
|
|
|
|
|
|
assert_eq!((world.width, world.height), (48, 32));
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
assert_eq!(world.person_actors.len(), 6);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
assert_eq!(world.food, 125);
|
|
|
|
|
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();
|
2026-10-12 14:25:51 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
state["limits"]["speed"],
|
|
|
|
|
default_config().max_simulation_speed
|
|
|
|
|
);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
|
|
|
|
|
let restored =
|
|
|
|
|
World::from_snapshot_with_script(&world.snapshot_json().unwrap(), 42, script).unwrap();
|
|
|
|
|
assert_eq!((restored.width, restored.height), (48, 32));
|
|
|
|
|
assert_eq!(simulation_state(&restored), simulation_state(&world));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn restored_snapshots_use_the_provided_script() {
|
|
|
|
|
let mut world = World::new(42);
|
2026-10-12 14:25:51 +00:00
|
|
|
let edited = with_constant(DEFAULT_SCRIPT, "MAX_SIMULATION_SPEED", "3");
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
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);
|
2026-10-12 14:25:51 +00:00
|
|
|
assert_eq!(
|
|
|
|
|
restored.config.max_simulation_speed,
|
|
|
|
|
default_config().max_simulation_speed
|
|
|
|
|
);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn behaviours_read_script_constants() {
|
|
|
|
|
let mut world = World::new(42);
|
2026-10-12 14:25:51 +00:00
|
|
|
let all_stay = with_constant(DEFAULT_SCRIPT, "RABBIT_BEHAVIOR_WANDER_CHANCE", "0.0");
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
world.set_script(&all_stay).unwrap();
|
|
|
|
|
let c = super::Ctx {
|
|
|
|
|
roll: 0.25,
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(world.decide("rabbit", c.clone()), "stay");
|
2026-10-12 14:25:51 +00:00
|
|
|
let all_wander = with_constant(DEFAULT_SCRIPT, "RABBIT_BEHAVIOR_WANDER_CHANCE", "1.0");
|
|
|
|
|
world.set_script(&all_wander).unwrap();
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
assert_eq!(world.decide("rabbit", c), "wander");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_rune_configuration_is_rejected() {
|
2026-10-12 14:25:51 +00:00
|
|
|
let source = with_constant(
|
|
|
|
|
&with_constant(DEFAULT_SCRIPT, "INITIAL_POPULATION", "3"),
|
|
|
|
|
"INITIAL_YOUNG_ADULTS",
|
|
|
|
|
"4",
|
|
|
|
|
);
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
assert!(
|
|
|
|
|
SimScript::load(&source)
|
|
|
|
|
.err()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("initial_young_adults cannot exceed initial_population")
|
|
|
|
|
);
|
2026-10-12 14:25:51 +00:00
|
|
|
let missing = DEFAULT_SCRIPT
|
|
|
|
|
.lines()
|
|
|
|
|
.filter(|line| !line.starts_with("pub const WIDTH = "))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("\n");
|
feat(sim)!: move simulation settings and behaviours into config.rn
Starting values and tuning constants are now `pub const` items in a
single Rune file, config.rn, alongside the `person`, `rabbit` and `wolf`
behaviour functions. The behaviours read the constants directly. At
startup the server loads the file from SIM_CONFIG (default: config.rn),
reads and checks every constant, and test-runs each behaviour, so a bad
or missing value stops startup with an error that names it.
- Map width and height can now be configured and are stored in
snapshots.
- The UI's edit limits (max speed, stock, inventory, hunger, energy,
resource amounts) come from the config.
- The in-browser script editor now edits config.rn. Applied edits update
behaviours and tuning values immediately; map size, initial population
and calendar defaults apply on reset.
- The Docker image includes config.rn, and Compose mounts the
repository's copy read-only.
- Documents the config file and how it works in the README.
BREAKING CHANGE: src/behavior.rn has been removed; behaviours now live
in config.rn. The server will not start without a valid config file.
Snapshots no longer store the behaviour script: on restart, config.rn
from disk replaces any edits made in the browser editor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 18:42:29 +00:00
|
|
|
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());
|
|
|
|
|
}
|
2026-10-08 06:44:30 +00:00
|
|
|
}
|