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;
|
2026-10-12 17:20:30 +00:00
|
|
|
#[allow(
|
|
|
|
|
dead_code,
|
|
|
|
|
reason = "the cluster gateway and shard nodes use the remote side"
|
|
|
|
|
)]
|
|
|
|
|
mod router;
|
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 actors::{
|
2026-10-12 17:20:30 +00:00
|
|
|
Activity, AnimalId, AnimalMsg, AnimalTurn, Area, BirthCost, Goods, Mailbox, Needs,
|
|
|
|
|
PartnershipMsg, PersonEdit, PersonMsg, ReplyBody, RequestId,
|
2026-10-11 15:29:17 +00:00
|
|
|
};
|
2026-10-12 17:20:30 +00:00
|
|
|
use picard::actors::{ActorRef, ActorSystem, Sender, Tell};
|
|
|
|
|
use router::{Arrival, LocalActor};
|
|
|
|
|
pub use router::{EntityKey, Kind, Router, Shard};
|
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;
|
2026-10-12 17:20:30 +00:00
|
|
|
use std::ops::Range;
|
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
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
impl Species {
|
|
|
|
|
fn kind(self) -> Kind {
|
|
|
|
|
match self {
|
|
|
|
|
Species::Rabbit => Kind::Rabbit,
|
|
|
|
|
Species::Wolf => Kind::Wolf,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How long a departed entity's actor keeps forwarding messages to the shard it moved to.
|
|
|
|
|
const FORWARDING_TICKS: u64 = 100;
|
|
|
|
|
|
|
|
|
|
/// An entity that moved to another shard and whose old actor still forwards its messages.
|
|
|
|
|
struct Departure {
|
|
|
|
|
key: EntityKey,
|
|
|
|
|
shard: usize,
|
|
|
|
|
tick: u64,
|
|
|
|
|
actor: LocalActor,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What changed in a shard during its steps, for the gateway to maintain the cluster directory.
|
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
|
|
|
pub struct StepReport {
|
|
|
|
|
/// Entities handed to another shard.
|
|
|
|
|
pub moved: Vec<(EntityKey, usize)>,
|
|
|
|
|
/// Entities that were born on, spawned on or moved into this shard.
|
|
|
|
|
pub arrived: Vec<EntityKey>,
|
|
|
|
|
/// Entities that died or were removed.
|
|
|
|
|
pub left: Vec<EntityKey>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reusable A* buffers; `visited` marks which entries belong to the current search.
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct PathScratch {
|
|
|
|
|
search: u32,
|
|
|
|
|
visited: Vec<u32>,
|
|
|
|
|
costs: Vec<usize>,
|
|
|
|
|
previous: Vec<usize>,
|
|
|
|
|
frontier: BinaryHeap<Reverse<(usize, usize)>>,
|
|
|
|
|
}
|
|
|
|
|
|
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-12 17:20:30 +00:00
|
|
|
shard: Shard,
|
|
|
|
|
router: Arc<Router>,
|
|
|
|
|
departed: Vec<Departure>,
|
|
|
|
|
report: StepReport,
|
|
|
|
|
paths: PathScratch,
|
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,
|
2026-10-12 17:20:30 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
next_animal_id: Option<AnimalId>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
shard: Option<Shard>,
|
2026-10-11 15:48:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
/// Drops partnerships with a member that is not `alive` and clears the partner links of
|
|
|
|
|
/// `people` in them; returns the dropped partnerships. With `clear_orphans`, which needs every
|
|
|
|
|
/// partnership to be known, links without any partnership are cleared too.
|
|
|
|
|
fn repair_partnerships(
|
|
|
|
|
people: &mut [Person],
|
|
|
|
|
partnerships: &mut Vec<Partnership>,
|
|
|
|
|
alive: impl Fn(u32) -> bool,
|
|
|
|
|
clear_orphans: bool,
|
|
|
|
|
) -> Vec<Partnership> {
|
|
|
|
|
let (kept, dissolved): (Vec<_>, Vec<_>) = partnerships.drain(..).partition(|partnership| {
|
|
|
|
|
alive(partnership.first_person_id) && alive(partnership.second_person_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
|
|
|
});
|
2026-10-12 17:20:30 +00:00
|
|
|
*partnerships = kept;
|
|
|
|
|
let pairs = |partnerships: &[Partnership]| -> HashSet<(u32, u32)> {
|
|
|
|
|
partnerships
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|partnership| (partnership.first_person_id, partnership.second_person_id))
|
|
|
|
|
.flat_map(|(first, second)| [(first, second), (second, first)])
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
|
|
|
|
let partnered = pairs(partnerships);
|
|
|
|
|
let broken = pairs(&dissolved);
|
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 person in people {
|
2026-10-12 17:20:30 +00:00
|
|
|
if person.partner_id.is_some_and(|partner| {
|
|
|
|
|
broken.contains(&(person.id, partner))
|
|
|
|
|
|| (clear_orphans && !partnered.contains(&(person.id, 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
|
|
|
person.partner_id = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
dissolved
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
let young_adults = self.shard.share(self.config.initial_young_adults);
|
|
|
|
|
for i in 0..self.shard.share(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-12 17:20:30 +00:00
|
|
|
let stage = if i < young_adults {
|
2026-10-11 19:23:50 +00:00
|
|
|
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
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
for _ in 0..self.shard.share(self.config.initial_rabbits) {
|
2026-10-11 19:23:50 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
for _ in 0..self.shard.share(self.config.initial_wolves) {
|
2026-10-11 19:23:50 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
fn empty(
|
|
|
|
|
seed: u64,
|
|
|
|
|
script: SimScript,
|
|
|
|
|
tiles: Vec<u8>,
|
|
|
|
|
res: Vec<(u8, u16)>,
|
|
|
|
|
shard: Shard,
|
|
|
|
|
) -> 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");
|
2026-10-12 17:20:30 +00:00
|
|
|
let first_id = shard.first_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
|
|
|
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(),
|
2026-10-12 17:20:30 +00:00
|
|
|
next_animal_id: first_id,
|
|
|
|
|
next_partnership_id: u32::try_from(first_id).expect("shard counts are small"),
|
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),
|
2026-10-12 17:20:30 +00:00
|
|
|
router: Router::new(shard, &actor_system),
|
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
|
|
|
actor_system,
|
2026-10-12 17:20:30 +00:00
|
|
|
shard,
|
|
|
|
|
departed: Vec::new(),
|
|
|
|
|
report: StepReport::default(),
|
|
|
|
|
paths: PathScratch::default(),
|
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),
|
2026-10-12 17:20:30 +00:00
|
|
|
food: u32::try_from(shard.share(config.initial_food as usize))
|
|
|
|
|
.expect("a share is at most the total"),
|
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-12 17:20:30 +00:00
|
|
|
next_id: u32::try_from(first_id).expect("shard counts are small"),
|
2026-10-07 16:04:37 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
#[cfg(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
|
|
|
pub fn new_with_script(seed: u64, script: SimScript) -> World {
|
2026-10-12 17:20:30 +00:00
|
|
|
Self::new_shard(seed, script, Shard::SINGLE)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates the part of the world owned by `shard`. Every shard generates the same terrain
|
|
|
|
|
/// and resources from `seed`, then keeps only the resources in its own columns.
|
|
|
|
|
pub fn new_shard(seed: u64, script: SimScript, shard: Shard) -> 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
|
|
|
let tiles = Self::generate_terrain(seed, &script.config);
|
|
|
|
|
let res = vec![(0, 0); tiles.len()];
|
2026-10-12 17:20:30 +00:00
|
|
|
let mut w = Self::empty(seed, script, tiles, res, shard);
|
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 (width, height) = (w.width, w.height);
|
2026-10-12 17:20:30 +00:00
|
|
|
let centre = w.region_centre();
|
2026-10-07 16:04:37 +00:00
|
|
|
w.home = w
|
|
|
|
|
.find_land(GRASS)
|
2026-10-12 17:20:30 +00:00
|
|
|
.unwrap_or_else(|| w.find_land(SAND).unwrap_or(centre));
|
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 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),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
let columns = w.columns();
|
|
|
|
|
for (i, res) in w.res.iter_mut().enumerate() {
|
|
|
|
|
if !columns.contains(&(i % width)) {
|
|
|
|
|
*res = (0, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-07 16:04:37 +00:00
|
|
|
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-12 17:20:30 +00:00
|
|
|
w.rng.0 ^= (shard.index as u64).wrapping_mul(0xD1B5_4A32_D192_ED03);
|
|
|
|
|
w.rng.0 |= 1;
|
2026-10-11 19:23:50 +00:00
|
|
|
w.populate();
|
2026-10-07 16:04:37 +00:00
|
|
|
w
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
/// The columns of the map this shard simulates.
|
|
|
|
|
fn columns(&self) -> Range<usize> {
|
|
|
|
|
self.shard.columns(self.width)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn owns(&self, (x, _): (usize, usize)) -> bool {
|
|
|
|
|
self.columns().contains(&x)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn region_centre(&self) -> (usize, usize) {
|
|
|
|
|
let columns = self.columns();
|
|
|
|
|
(columns.start.midpoint(columns.end), self.height / 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn take_person_id(&mut self) -> u32 {
|
|
|
|
|
let id = self.next_id;
|
|
|
|
|
self.next_id += u32::try_from(self.shard.id_stride()).expect("shard counts are small");
|
|
|
|
|
id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn take_partnership_id(&mut self) -> u32 {
|
|
|
|
|
let id = self.next_partnership_id;
|
|
|
|
|
self.next_partnership_id +=
|
|
|
|
|
u32::try_from(self.shard.id_stride()).expect("shard counts are small");
|
|
|
|
|
id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Records an entity appearing on this shard, unless it is the only one.
|
|
|
|
|
fn report_arrival(&mut self, key: EntityKey) {
|
|
|
|
|
if self.shard.count > 1 {
|
|
|
|
|
self.report.arrived.push(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn report_departure(&mut self, key: EntityKey) {
|
|
|
|
|
if self.shard.count > 1 {
|
|
|
|
|
self.report.left.push(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2026-10-12 17:20:30 +00:00
|
|
|
.router
|
|
|
|
|
.spawn_person(person)
|
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
|
|
|
.expect("failed to create Picard person actor");
|
|
|
|
|
self.person_actors.insert(id, actor);
|
2026-10-12 17:20:30 +00:00
|
|
|
self.report_arrival(EntityKey::person(id));
|
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;
|
2026-10-12 17:20:30 +00:00
|
|
|
self.next_animal_id += self.shard.id_stride();
|
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 = self
|
2026-10-12 17:20:30 +00:00
|
|
|
.router
|
|
|
|
|
.spawn_animal(species.kind(), id, animal)
|
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
|
|
|
.expect("failed to create Picard animal actor");
|
|
|
|
|
self.animal_actors_mut(species).insert(id, actor);
|
2026-10-12 17:20:30 +00:00
|
|
|
self.report_arrival(EntityKey::new(species.kind(), 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
|
|
|
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
|
2026-10-12 17:20:30 +00:00
|
|
|
.router
|
|
|
|
|
.spawn_partnership(partnership)
|
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
|
|
|
.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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
let key = EntityKey::person(id);
|
|
|
|
|
self.router.unregister(key);
|
|
|
|
|
LocalActor::Person(actor).stop();
|
|
|
|
|
self.report_departure(key);
|
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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
let key = EntityKey::new(species.kind(), id);
|
|
|
|
|
self.router.unregister(key);
|
|
|
|
|
LocalActor::Animal(actor).stop();
|
|
|
|
|
self.report_departure(key);
|
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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
self.router
|
|
|
|
|
.unregister(EntityKey::new(Kind::Partnership, u64::from(id)));
|
|
|
|
|
LocalActor::Partnership(actor).stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Takes in the entities other shards handed over since the last step.
|
|
|
|
|
fn receive_arrivals(&mut self) {
|
|
|
|
|
for arrival in self.router.take_arrivals() {
|
|
|
|
|
let key = match arrival {
|
|
|
|
|
Arrival::Person(person, actor) => {
|
|
|
|
|
self.person_actors.insert(person.id, actor);
|
|
|
|
|
self.say(&format!("Person #{} arrived from afar", person.id));
|
|
|
|
|
EntityKey::person(person.id)
|
|
|
|
|
}
|
|
|
|
|
Arrival::Animal(kind, id, actor) => {
|
|
|
|
|
let species = if kind == Kind::Wolf {
|
|
|
|
|
Species::Wolf
|
|
|
|
|
} else {
|
|
|
|
|
Species::Rabbit
|
|
|
|
|
};
|
|
|
|
|
self.animal_actors_mut(species).insert(id, actor);
|
|
|
|
|
EntityKey::new(kind, id)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.departed.retain(|departure| departure.key != key);
|
|
|
|
|
self.report_arrival(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stops the actors of entities that left long enough ago for every shard to know where
|
|
|
|
|
/// they went; the router keeps forwarding their messages from then on.
|
|
|
|
|
fn retire_departures(&mut self) {
|
|
|
|
|
let tick = self.tick;
|
|
|
|
|
let (expired, waiting): (Vec<_>, Vec<_>) = self
|
|
|
|
|
.departed
|
|
|
|
|
.drain(..)
|
|
|
|
|
.partition(|departure| tick.saturating_sub(departure.tick) >= FORWARDING_TICKS);
|
|
|
|
|
self.departed = waiting;
|
|
|
|
|
for departure in expired {
|
|
|
|
|
if self.router.retire_forwarder(departure.key, departure.shard) {
|
|
|
|
|
departure.actor.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Hands every entity standing outside this shard's columns to the shard that owns its
|
|
|
|
|
/// position. The entities' actors pass their whole state on themselves.
|
|
|
|
|
fn transfer_out(&mut self, census: &Census) {
|
|
|
|
|
if self.shard.count == 1 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let request = self.mailbox.request();
|
|
|
|
|
let mut leaving = Vec::new();
|
|
|
|
|
for person in &census.people {
|
|
|
|
|
if !self.owns(person.position())
|
|
|
|
|
&& let Some(actor) = self.person_actors.remove(&person.id)
|
|
|
|
|
{
|
|
|
|
|
let shard = self.shard.owner(person.x, self.width);
|
|
|
|
|
actor.tell(PersonMsg::Depart(request, shard), self.mailbox.reply_to());
|
|
|
|
|
leaving.push((
|
|
|
|
|
EntityKey::person(person.id),
|
|
|
|
|
shard,
|
|
|
|
|
LocalActor::Person(actor),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for species in [Species::Rabbit, Species::Wolf] {
|
|
|
|
|
let animals = match species {
|
|
|
|
|
Species::Rabbit => &census.rabbits,
|
|
|
|
|
Species::Wolf => &census.wolves,
|
|
|
|
|
};
|
|
|
|
|
for (id, animal) in animals {
|
|
|
|
|
if !self.owns(animal.position())
|
|
|
|
|
&& let Some(actor) = self.animal_actors_mut(species).remove(id)
|
|
|
|
|
{
|
|
|
|
|
let shard = self.shard.owner(animal.x, self.width);
|
|
|
|
|
actor.tell(AnimalMsg::Depart(request, shard), self.mailbox.reply_to());
|
|
|
|
|
let key = EntityKey::new(species.kind(), *id);
|
|
|
|
|
leaving.push((key, shard, LocalActor::Animal(actor)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.mailbox.collect(request, leaving.len());
|
|
|
|
|
for (key, shard, actor) in leaving {
|
|
|
|
|
self.report.moved.push((key, shard));
|
|
|
|
|
self.departed.push(Departure {
|
|
|
|
|
key,
|
|
|
|
|
shard,
|
|
|
|
|
tick: self.tick,
|
|
|
|
|
actor,
|
|
|
|
|
});
|
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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
self.router.tell(EntityKey::person(id), message);
|
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)> {
|
2026-10-12 17:20:30 +00:00
|
|
|
let c = self.region_centre();
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
for x in self.columns() {
|
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.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)> {
|
2026-10-12 17:20:30 +00:00
|
|
|
let columns = self.columns();
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
let x = columns.start + self.rng.below(columns.len());
|
|
|
|
|
let y = 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);
|
2026-10-12 17:20:30 +00:00
|
|
|
Person {
|
|
|
|
|
id: self.take_person_id(),
|
2026-10-07 16:04:37 +00:00
|
|
|
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-12 17:20:30 +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_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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
let mut paths = std::mem::take(&mut self.paths);
|
|
|
|
|
let step = self.find_path_step(&mut paths, from, to);
|
|
|
|
|
self.paths = paths;
|
|
|
|
|
step.unwrap_or_else(|| self.wander(from.0, from.1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A* search from `from` to `to`; returns the first step of a shortest path.
|
|
|
|
|
fn find_path_step(
|
|
|
|
|
&self,
|
|
|
|
|
paths: &mut PathScratch,
|
|
|
|
|
from: (usize, usize),
|
|
|
|
|
to: (usize, usize),
|
|
|
|
|
) -> Option<(usize, usize)> {
|
|
|
|
|
let tiles = self.width * self.height;
|
|
|
|
|
if paths.visited.len() != tiles {
|
|
|
|
|
*paths = PathScratch {
|
|
|
|
|
visited: vec![0; tiles],
|
|
|
|
|
costs: vec![0; tiles],
|
|
|
|
|
previous: vec![0; tiles],
|
|
|
|
|
..PathScratch::default()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
paths.search = paths.search.wrapping_add(1);
|
|
|
|
|
if paths.search == 0 {
|
|
|
|
|
paths.visited.fill(0);
|
|
|
|
|
paths.search = 1;
|
|
|
|
|
}
|
|
|
|
|
let search = paths.search;
|
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-12 17:20:30 +00:00
|
|
|
paths.frontier.clear();
|
|
|
|
|
paths.frontier.push(Reverse((cheb(from, to), start)));
|
|
|
|
|
paths.visited[start] = search;
|
|
|
|
|
paths.costs[start] = 0;
|
|
|
|
|
paths.previous[start] = start;
|
2026-10-11 16:39:50 +00:00
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
while let Some(Reverse((_, current))) = paths.frontier.pop() {
|
2026-10-11 16:39:50 +00:00
|
|
|
if current == target {
|
|
|
|
|
let mut next = target;
|
2026-10-12 17:20:30 +00:00
|
|
|
while paths.previous[next] != start {
|
|
|
|
|
next = paths.previous[next];
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
return Some((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 {
|
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-12 17:20:30 +00:00
|
|
|
let cost = paths.costs[current] + 1;
|
|
|
|
|
if paths.visited[neighbor] == search && cost >= paths.costs[neighbor] {
|
2026-10-11 16:39:50 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
paths.visited[neighbor] = search;
|
|
|
|
|
paths.costs[neighbor] = cost;
|
|
|
|
|
paths.previous[neighbor] = current;
|
|
|
|
|
paths
|
|
|
|
|
.frontier
|
|
|
|
|
.push(Reverse((cost + cheb((x, y), to), neighbor)));
|
2026-10-11 16:39:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
None
|
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;
|
2026-10-12 17:20:30 +00:00
|
|
|
self.receive_arrivals();
|
|
|
|
|
self.retire_departures();
|
2026-10-07 16:04:37 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
let new_day = self.tick.is_multiple_of(self.calendar.ticks_per_day);
|
|
|
|
|
if self.retire_dead(&mut census) || (self.shard.count > 1 && new_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.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-12 17:20:30 +00:00
|
|
|
self.transfer_out(&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 partners_before: Vec<Option<u32>> = people.iter().map(|p| p.partner_id).collect();
|
2026-10-12 17:20:30 +00:00
|
|
|
let local: HashSet<u32> = people.iter().map(|p| p.id).collect();
|
|
|
|
|
let router = Arc::clone(&self.router);
|
|
|
|
|
let alive = |id: u32| local.contains(&id) || router.knows(EntityKey::person(id));
|
|
|
|
|
let dissolved =
|
|
|
|
|
repair_partnerships(people, &mut partnerships, alive, self.shard.count == 1);
|
|
|
|
|
for partnership in dissolved {
|
|
|
|
|
self.retire_partnership(partnership.id);
|
|
|
|
|
for member in [partnership.first_person_id, partnership.second_person_id] {
|
|
|
|
|
if !local.contains(&member) && router.knows(EntityKey::person(member)) {
|
|
|
|
|
self.tell_person(member, PersonMsg::SetPartner(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 (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) {
|
2026-10-12 17:20:30 +00:00
|
|
|
let columns = self.columns();
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
let tile = self.rng.below(columns.len() * self.height);
|
|
|
|
|
let i = (tile / columns.len()) * self.width + columns.start + tile % columns.len();
|
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;
|
2026-10-12 17:20:30 +00:00
|
|
|
if census.rabbits.len() < self.shard.share(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);
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
if census.wolves.len() < self.shard.share(self.config.wolf_spawn_cap)
|
|
|
|
|
&& census.rabbits.len() > self.shard.share(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)));
|
2026-10-12 17:20:30 +00:00
|
|
|
let id = self.take_partnership_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
|
|
|
self.spawn_partnership_actor(Partnership {
|
2026-10-12 17:20:30 +00:00
|
|
|
id,
|
2026-10-11 15:48:26 +00:00
|
|
|
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
|
|
|
});
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
self.shard.share(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,
|
2026-10-12 17:20:30 +00:00
|
|
|
self.shard.share(self.config.rabbit_max_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
|
|
|
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,
|
2026-10-12 17:20:30 +00:00
|
|
|
self.shard.share(self.config.wolf_max_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
|
|
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
let mut world = Self::new_shard(seed, self.script.clone(), self.shard);
|
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-12 17:20:30 +00:00
|
|
|
next_animal_id: Some(self.next_animal_id),
|
|
|
|
|
shard: Some(self.shard),
|
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> {
|
2026-10-12 17:20:30 +00:00
|
|
|
Self::from_snapshot_with_script(snapshot, fallback_seed, default_script(), Shard::SINGLE)
|
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-12 17:20:30 +00:00
|
|
|
/// Restores world state from a snapshot of `shard`; `script` (normally `config.rn`
|
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
|
|
|
/// on disk) supplies the behaviours and tuning constants.
|
|
|
|
|
pub fn from_snapshot_with_script(
|
|
|
|
|
snapshot: &str,
|
|
|
|
|
fallback_seed: u64,
|
|
|
|
|
script: SimScript,
|
2026-10-12 17:20:30 +00:00
|
|
|
shard: Shard,
|
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
|
|
|
) -> 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());
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
let saved_shard = saved.shard.unwrap_or(Shard::SINGLE);
|
|
|
|
|
if saved_shard != shard {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"world snapshot belongs to shard {} of {}, not {} of {}",
|
|
|
|
|
saved_shard.index, saved_shard.count, shard.index, shard.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
|
|
|
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,
|
2026-10-12 17:20:30 +00:00
|
|
|
shard,
|
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.width = saved.width;
|
|
|
|
|
world.height = saved.height;
|
|
|
|
|
world.next_partnership_id = saved.next_partnership_id;
|
2026-10-12 17:20:30 +00:00
|
|
|
if let Some(next_animal_id) = saved.next_animal_id {
|
|
|
|
|
world.next_animal_id = next_animal_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
|
|
|
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;
|
2026-10-12 17:20:30 +00:00
|
|
|
// Other shards are unknown until the cluster directory arrives; partnerships with
|
|
|
|
|
// members elsewhere are checked by the daily pruning instead.
|
|
|
|
|
let local: HashSet<u32> = people.iter().map(|person| person.id).collect();
|
|
|
|
|
let single = shard.count == 1;
|
|
|
|
|
repair_partnerships(
|
|
|
|
|
&mut people,
|
|
|
|
|
&mut partnerships,
|
|
|
|
|
|id| !single || local.contains(&id),
|
|
|
|
|
single,
|
|
|
|
|
);
|
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 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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
if !self.owns((x, y)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-10-11 19:23:50 +00:00
|
|
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
if left == 0 || !self.walkable((x, y)) || !self.owns((x, y)) {
|
2026-10-11 19:23:50 +00:00
|
|
|
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
|
2026-10-12 17:20:30 +00:00
|
|
|
&& self.owns((x, y))
|
2026-10-11 19:23:50 +00:00
|
|
|
&& 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-12 17:20:30 +00:00
|
|
|
/// The interface the cluster uses to drive a shard.
|
|
|
|
|
#[allow(dead_code, reason = "the cluster gateway and shard nodes use these")]
|
|
|
|
|
impl World {
|
|
|
|
|
pub fn shard(&self) -> Shard {
|
|
|
|
|
self.shard
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn router(&self) -> &Arc<Router> {
|
|
|
|
|
&self.router
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns and resets what changed since the last report.
|
|
|
|
|
pub fn take_report(&mut self) -> StepReport {
|
|
|
|
|
std::mem::take(&mut self.report)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Every person and animal currently simulated by this shard.
|
|
|
|
|
pub fn roster(&self) -> Vec<EntityKey> {
|
|
|
|
|
let people = self.person_actors.keys().map(|&id| EntityKey::person(id));
|
|
|
|
|
let rabbits = self
|
|
|
|
|
.rabbit_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Rabbit, id));
|
|
|
|
|
let wolves = self
|
|
|
|
|
.wolf_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Wolf, id));
|
|
|
|
|
people.chain(rabbits).chain(wolves).collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-10-12 17:20:30 +00:00
|
|
|
use super::actors::{Activity, AnimalTurn, Goods, PersonEdit, PersonMsg, ReplyBody};
|
|
|
|
|
use super::{
|
|
|
|
|
Animal, DEFAULT_SCRIPT, EntityKey, FORWARDING_TICKS, H, LifeStage, Router, Shard,
|
|
|
|
|
SimScript, W, World, cheb, default_config,
|
|
|
|
|
};
|
|
|
|
|
use crate::world::router::RemoteSender;
|
|
|
|
|
use picard::remoting::{ActorAddress, RemoteRequest};
|
|
|
|
|
use std::sync::{Arc, Mutex, Weak};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
/// Delivers remote messages by calling the receiving shard's router directly.
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct Loopback {
|
|
|
|
|
routers: Mutex<Vec<Weak<Router>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RemoteSender for Loopback {
|
|
|
|
|
fn send(&self, shard: usize, path: &str, payload: serde_json::Value) {
|
|
|
|
|
let router = self.routers.lock().unwrap()[shard].upgrade().unwrap();
|
|
|
|
|
let destination = ActorAddress::local(router.system_id(), path);
|
|
|
|
|
let response =
|
|
|
|
|
router.handle("loopback", RemoteRequest::new(destination, None, payload));
|
|
|
|
|
assert!(response.accepted, "{path} rejected: {:?}", response.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn flush(&self) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn connected_shards(count: usize) -> Vec<World> {
|
|
|
|
|
let loopback = Arc::new(Loopback::default());
|
|
|
|
|
let shards: Vec<World> = (0..count)
|
|
|
|
|
.map(|index| {
|
|
|
|
|
let shard = Shard::new(index, count).unwrap();
|
|
|
|
|
World::new_shard(42, SimScript::load(DEFAULT_SCRIPT).unwrap(), shard)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
for world in &shards {
|
|
|
|
|
loopback
|
|
|
|
|
.routers
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.push(Arc::downgrade(world.router()));
|
|
|
|
|
world.router().set_remote(loopback.clone());
|
|
|
|
|
}
|
|
|
|
|
shards
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Does what the gateway does after every step: publishes where entities went.
|
|
|
|
|
fn publish_directory(shards: &mut [World]) {
|
|
|
|
|
let mut changes = Vec::new();
|
|
|
|
|
for (index, world) in shards.iter_mut().enumerate() {
|
|
|
|
|
let report = world.take_report();
|
|
|
|
|
changes.extend(report.moved.into_iter().map(|(key, to)| (key, Some(to))));
|
|
|
|
|
changes.extend(report.arrived.into_iter().map(|key| (key, Some(index))));
|
|
|
|
|
changes.extend(report.left.into_iter().map(|key| (key, None)));
|
|
|
|
|
}
|
|
|
|
|
for world in shards {
|
|
|
|
|
world.router().apply_directory(&changes);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait_for_person(world: &World, id: u32, done: impl Fn(&super::Person) -> bool) {
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
|
|
|
loop {
|
|
|
|
|
let people = world.observe_people();
|
|
|
|
|
if people.iter().any(|person| person.id == id && done(person)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
assert!(
|
|
|
|
|
Instant::now() < deadline,
|
|
|
|
|
"person #{id} never got the message"
|
|
|
|
|
);
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
}
|
|
|
|
|
}
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
/// The default script at the size of the original single village, for long-running tests.
|
|
|
|
|
fn village_script() -> SimScript {
|
|
|
|
|
let source = [
|
|
|
|
|
("WIDTH", "96"),
|
|
|
|
|
("HEIGHT", "64"),
|
|
|
|
|
("INITIAL_POPULATION", "90"),
|
|
|
|
|
("INITIAL_YOUNG_ADULTS", "30"),
|
|
|
|
|
("INITIAL_RABBITS", "600"),
|
|
|
|
|
("INITIAL_WOLVES", "40"),
|
|
|
|
|
("INITIAL_FOOD", "600"),
|
|
|
|
|
("POPULATION_CAPACITY", "12"),
|
|
|
|
|
("MAX_HOUSES", "10"),
|
|
|
|
|
("RABBIT_SPAWN_CAP", "15"),
|
|
|
|
|
("WOLF_SPAWN_CAP", "2"),
|
|
|
|
|
("WOLF_SPAWN_RABBIT_THRESHOLD", "40"),
|
|
|
|
|
("RABBIT_MAX_POPULATION", "120"),
|
|
|
|
|
("WOLF_MAX_POPULATION", "10"),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.fold(DEFAULT_SCRIPT.to_string(), |source, (name, value)| {
|
|
|
|
|
with_constant(&source, name, value)
|
|
|
|
|
});
|
|
|
|
|
SimScript::load(&source).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 14:25:51 +00:00
|
|
|
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-12 17:20:30 +00:00
|
|
|
#[test]
|
|
|
|
|
fn shards_split_resources_and_entities_without_overlap() {
|
|
|
|
|
let single = World::new(42);
|
|
|
|
|
let shards = connected_shards(3);
|
|
|
|
|
let width = single.width;
|
|
|
|
|
for (index, res) in single.res.iter().enumerate() {
|
|
|
|
|
let owners = shards.iter().filter(|w| w.res[index].0 != 0).count();
|
|
|
|
|
assert!(owners <= 1, "tile {index} has resources on several shards");
|
|
|
|
|
let position = (index % width, index / width);
|
|
|
|
|
if res.0 != 0 && position != single.home {
|
|
|
|
|
let expected = &shards[Shard::new(0, 3).unwrap().owner(position.0, width)];
|
|
|
|
|
assert!(position == expected.home || expected.res[index] == *res);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut ids: Vec<EntityKey> = shards.iter().flat_map(World::roster).collect();
|
|
|
|
|
let total = ids.len();
|
|
|
|
|
ids.sort_unstable();
|
|
|
|
|
ids.dedup();
|
|
|
|
|
assert_eq!(ids.len(), total, "entity ids must be unique across shards");
|
|
|
|
|
let people: usize = shards.iter().map(|w| w.person_actors.len()).sum();
|
|
|
|
|
assert_eq!(people, single.person_actors.len());
|
|
|
|
|
for world in &shards {
|
|
|
|
|
assert!(world.owns(world.home));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn people_cross_shards_with_their_inventory_and_stay_reachable() {
|
|
|
|
|
let mut shards = connected_shards(2);
|
|
|
|
|
publish_directory(&mut shards);
|
|
|
|
|
let id = *shards[0].person_actors.keys().next().unwrap();
|
|
|
|
|
let destination = shards[1].home;
|
|
|
|
|
shards[0].tell_person(id, PersonMsg::Edit(PersonEdit::Wood(7)));
|
|
|
|
|
shards[0].tell_person(id, PersonMsg::MoveTo(destination));
|
|
|
|
|
shards[0].step();
|
|
|
|
|
assert!(!shards[0].person_actors.contains_key(&id));
|
|
|
|
|
let report = shards[0].take_report();
|
|
|
|
|
assert_eq!(report.moved, vec![(EntityKey::person(id), 1)]);
|
|
|
|
|
|
|
|
|
|
shards[1].receive_arrivals();
|
|
|
|
|
assert!(shards[1].person_actors.contains_key(&id));
|
|
|
|
|
let arrived = shards[1]
|
|
|
|
|
.observe_people()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|person| person.id == id)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(arrived.wood, 7);
|
|
|
|
|
assert!(shards[1].owns(arrived.position()));
|
|
|
|
|
|
|
|
|
|
// The old actor forwards messages sent to the person on the shard they left.
|
|
|
|
|
shards[0].tell_person(id, PersonMsg::Edit(PersonEdit::Stone(3)));
|
|
|
|
|
wait_for_person(&shards[1], id, |person| person.stone == 3);
|
|
|
|
|
|
|
|
|
|
// Once it stops, the router forwards them itself until the directory catches up.
|
|
|
|
|
shards[0].tick += FORWARDING_TICKS;
|
|
|
|
|
shards[0].retire_departures();
|
|
|
|
|
assert!(shards[0].departed.is_empty());
|
|
|
|
|
shards[0].tell_person(id, PersonMsg::Edit(PersonEdit::Stone(4)));
|
|
|
|
|
wait_for_person(&shards[1], id, |person| person.stone == 4);
|
|
|
|
|
|
|
|
|
|
publish_directory(&mut shards);
|
|
|
|
|
shards[0].tell_person(id, PersonMsg::Edit(PersonEdit::Stone(5)));
|
|
|
|
|
wait_for_person(&shards[1], id, |person| person.stone == 5);
|
|
|
|
|
|
|
|
|
|
// Walking back resumes life on the first shard.
|
|
|
|
|
let back = shards[0].home;
|
|
|
|
|
shards[1].tell_person(id, PersonMsg::MoveTo(back));
|
|
|
|
|
shards[1].step();
|
|
|
|
|
shards[0].receive_arrivals();
|
|
|
|
|
assert!(shards[0].person_actors.contains_key(&id));
|
|
|
|
|
assert!(!shards[1].person_actors.contains_key(&id));
|
|
|
|
|
publish_directory(&mut shards);
|
|
|
|
|
shards[1].tell_person(id, PersonMsg::Edit(PersonEdit::Stone(6)));
|
|
|
|
|
wait_for_person(&shards[0], id, |person| person.stone == 6);
|
|
|
|
|
}
|
|
|
|
|
|
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() {
|
2026-10-12 17:20:30 +00:00
|
|
|
let mut world = World::new_with_script(42, village_script());
|
2026-10-11 16:39:50 +00:00
|
|
|
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() {
|
2026-10-12 17:20:30 +00:00
|
|
|
let mut world = World::new_with_script(42, village_script());
|
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
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
let restored = World::from_snapshot_with_script(
|
|
|
|
|
&world.snapshot_json().unwrap(),
|
|
|
|
|
42,
|
|
|
|
|
script,
|
|
|
|
|
Shard::SINGLE,
|
|
|
|
|
)
|
|
|
|
|
.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.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
|
|
|
}
|