refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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;
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
mod economy;
|
2026-10-12 17:20:30 +00:00
|
|
|
mod router;
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
mod work;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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 18:29:11 +00:00
|
|
|
pub use economy::Quote;
|
|
|
|
|
use economy::{Cart, CartMsg, Job, Market, MarketMsg, Workplace, WorkplaceMsg};
|
2026-10-12 17:20:30 +00:00
|
|
|
use picard::actors::{ActorRef, ActorSystem, Sender, Tell};
|
|
|
|
|
use router::{Arrival, LocalActor};
|
2026-10-12 18:29:11 +00:00
|
|
|
pub use router::{EntityKey, Kind, RemoteSender, 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
|
|
|
|
2026-10-12 18:29:11 +00:00
|
|
|
/// A random seed for a new world that differs from `current`.
|
|
|
|
|
pub fn regenerated_seed(current: u64) -> u64 {
|
|
|
|
|
let mut seed = rand::random::<u64>();
|
|
|
|
|
if seed == current {
|
|
|
|
|
seed = seed.wrapping_add(1);
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
/// Base price and the stock a market aims to hold of one good.
|
|
|
|
|
#[derive(Clone, Copy, Debug, rune::FromValue)]
|
|
|
|
|
pub struct GoodSettings {
|
|
|
|
|
pub price: u32,
|
|
|
|
|
pub target: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How many workplaces of one trade each shard founds, their positions and the daily wage.
|
|
|
|
|
#[derive(Clone, Copy, Debug, rune::FromValue)]
|
|
|
|
|
pub struct TradeSettings {
|
|
|
|
|
pub count: usize,
|
|
|
|
|
pub workers: usize,
|
|
|
|
|
pub wage: u32,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:38:57 +00:00
|
|
|
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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
work_start: f32,
|
|
|
|
|
work_end: f32,
|
|
|
|
|
carry_capacity: u32,
|
|
|
|
|
cart_capacity: u32,
|
|
|
|
|
cart_durability: u32,
|
|
|
|
|
cart_wood_cost: u32,
|
|
|
|
|
initial_person_coins: u32,
|
|
|
|
|
initial_workplace_coins: u32,
|
|
|
|
|
initial_market_coins: u32,
|
|
|
|
|
quit_after_unpaid_days: u32,
|
|
|
|
|
household_food_target: u32,
|
|
|
|
|
household_coin_reserve: u32,
|
|
|
|
|
cart_purchase_coins: u32,
|
|
|
|
|
work_radius: usize,
|
|
|
|
|
field_radius: usize,
|
|
|
|
|
crop_ripe: u16,
|
|
|
|
|
crop_yield: u32,
|
|
|
|
|
craft_budget: u32,
|
|
|
|
|
merchant_escrow: u32,
|
|
|
|
|
haggle_rounds: u32,
|
|
|
|
|
price_food: GoodSettings,
|
|
|
|
|
price_wood: GoodSettings,
|
|
|
|
|
price_stone: GoodSettings,
|
|
|
|
|
price_tools: GoodSettings,
|
|
|
|
|
price_carts: GoodSettings,
|
|
|
|
|
trade_farm: TradeSettings,
|
|
|
|
|
trade_woodcutter: TradeSettings,
|
|
|
|
|
trade_quarry: TradeSettings,
|
|
|
|
|
trade_hunter: TradeSettings,
|
|
|
|
|
trade_toolmaker: TradeSettings,
|
|
|
|
|
trade_cartwright: TradeSettings,
|
|
|
|
|
trade_merchant: TradeSettings,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()?;
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
self.validate_amounts()?;
|
|
|
|
|
self.validate_economy()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_economy(&self) -> Result<(), String> {
|
|
|
|
|
if !(0.0..1.0).contains(&self.work_start)
|
|
|
|
|
|| !(self.work_start..=1.0).contains(&self.work_end)
|
|
|
|
|
|| self.work_start >= self.work_end
|
|
|
|
|
{
|
|
|
|
|
return Err("work_start and work_end must satisfy 0 <= start < end <= 1".into());
|
|
|
|
|
}
|
|
|
|
|
if self.carry_capacity == 0
|
|
|
|
|
|| self.carry_capacity > 10_000
|
|
|
|
|
|| self.cart_capacity > 10_000
|
|
|
|
|
|| self.cart_durability == 0
|
|
|
|
|
|| self.cart_wood_cost == 0
|
|
|
|
|
|| self.crop_ripe == 0
|
|
|
|
|
|| !(1..=100).contains(&self.haggle_rounds)
|
|
|
|
|
|| self.work_radius > 64
|
|
|
|
|
|| self.field_radius > 8
|
|
|
|
|
{
|
|
|
|
|
return Err("economy capacities, costs and radii are outside their ranges".into());
|
|
|
|
|
}
|
|
|
|
|
for (name, value) in [
|
|
|
|
|
("initial_person_coins", self.initial_person_coins),
|
|
|
|
|
("initial_workplace_coins", self.initial_workplace_coins),
|
|
|
|
|
("initial_market_coins", self.initial_market_coins),
|
|
|
|
|
("household_food_target", self.household_food_target),
|
|
|
|
|
("household_coin_reserve", self.household_coin_reserve),
|
|
|
|
|
("cart_purchase_coins", self.cart_purchase_coins),
|
|
|
|
|
("crop_yield", self.crop_yield),
|
|
|
|
|
("craft_budget", self.craft_budget),
|
|
|
|
|
("merchant_escrow", self.merchant_escrow),
|
|
|
|
|
] {
|
|
|
|
|
if value > 1_000_000 {
|
|
|
|
|
return Err(format!("{name} must be at most 1000000"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (name, good) in [
|
|
|
|
|
("price_food", self.price_food),
|
|
|
|
|
("price_wood", self.price_wood),
|
|
|
|
|
("price_stone", self.price_stone),
|
|
|
|
|
("price_tools", self.price_tools),
|
|
|
|
|
("price_carts", self.price_carts),
|
|
|
|
|
] {
|
|
|
|
|
if !(1..=100_000).contains(&good.price) || !(1..=100_000).contains(&good.target) {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"{name} needs a price and target between 1 and 100000"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (name, trade) in self.trades() {
|
|
|
|
|
if trade.count > 16 || trade.workers > 100 || trade.wage > 100_000 {
|
|
|
|
|
return Err(format!(
|
|
|
|
|
"trade_{name} allows at most 16 sites, 100 workers and a wage of 100000"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if self.trade_merchant.count > 1 {
|
|
|
|
|
return Err("trade_merchant.count is the number of markets per shard: 0 or 1".into());
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn trades(&self) -> [(&'static str, TradeSettings); 7] {
|
|
|
|
|
[
|
|
|
|
|
("farm", self.trade_farm),
|
|
|
|
|
("woodcutter", self.trade_woodcutter),
|
|
|
|
|
("quarry", self.trade_quarry),
|
|
|
|
|
("hunter", self.trade_hunter),
|
|
|
|
|
("toolmaker", self.trade_toolmaker),
|
|
|
|
|
("cartwright", self.trade_cartwright),
|
|
|
|
|
("merchant", self.trade_merchant),
|
|
|
|
|
]
|
2026-10-11 19:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
"work_start",
|
|
|
|
|
"work_end",
|
|
|
|
|
"carry_capacity",
|
|
|
|
|
"cart_capacity",
|
|
|
|
|
"cart_durability",
|
|
|
|
|
"cart_wood_cost",
|
|
|
|
|
"initial_person_coins",
|
|
|
|
|
"initial_workplace_coins",
|
|
|
|
|
"initial_market_coins",
|
|
|
|
|
"quit_after_unpaid_days",
|
|
|
|
|
"household_food_target",
|
|
|
|
|
"household_coin_reserve",
|
|
|
|
|
"cart_purchase_coins",
|
|
|
|
|
"work_radius",
|
|
|
|
|
"field_radius",
|
|
|
|
|
"crop_ripe",
|
|
|
|
|
"crop_yield",
|
|
|
|
|
"craft_budget",
|
|
|
|
|
"merchant_escrow",
|
|
|
|
|
"haggle_rounds",
|
|
|
|
|
"price_food",
|
|
|
|
|
"price_wood",
|
|
|
|
|
"price_stone",
|
|
|
|
|
"price_tools",
|
|
|
|
|
"price_carts",
|
|
|
|
|
"trade_farm",
|
|
|
|
|
"trade_woodcutter",
|
|
|
|
|
"trade_quarry",
|
|
|
|
|
"trade_hunter",
|
|
|
|
|
"trade_toolmaker",
|
|
|
|
|
"trade_cartwright",
|
|
|
|
|
"trade_merchant",
|
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
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-10-12 18:29:11 +00:00
|
|
|
#[cfg_attr(not(test), expect(dead_code, reason = "tests read the source back"))]
|
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
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 18:29:11 +00:00
|
|
|
#[cfg(test)]
|
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 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;
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
/// A sown field tile; its amount is how far the crop has grown.
|
|
|
|
|
const CROP: u8 = 4;
|
2026-10-07 16:04:37 +00:00
|
|
|
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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
#[rune(get)]
|
|
|
|
|
employed: bool,
|
|
|
|
|
/// Whether it is working hours.
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
work_time: bool,
|
|
|
|
|
/// Whether the person's shift for today is over.
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
shift_done: bool,
|
|
|
|
|
/// Whether the person is in the middle of a shift and should finish it.
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
on_shift: bool,
|
|
|
|
|
/// Whether the person is a merchant on a trading trip.
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
on_trip: bool,
|
|
|
|
|
#[rune(get)]
|
|
|
|
|
coins: i64,
|
2026-10-07 16:42:59 +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 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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
tools: u32,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
coins: u32,
|
|
|
|
|
/// The village the person lives in; `None` in snapshots from before people had homes.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
home: Option<(usize, usize)>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
job: Option<Job>,
|
|
|
|
|
/// The cart the person owns.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
cart: Option<u64>,
|
|
|
|
|
/// Shifts in a row the employer could not pay in full.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
unpaid_days: u32,
|
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 {
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
self.food + self.wood + self.stone + self.tools
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Everything the person carries, their own goods and their employer's.
|
|
|
|
|
fn load(&self) -> u32 {
|
|
|
|
|
self.carry() + self.job.as_ref().map_or(0, |job| job.goods.total())
|
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 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)>,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
carts: Vec<Cart>,
|
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>>,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
workplace_actors: BTreeMap<u64, ActorRef<WorkplaceMsg>>,
|
|
|
|
|
market_actors: BTreeMap<u64, ActorRef<MarketMsg>>,
|
|
|
|
|
cart_actors: BTreeMap<u64, ActorRef<CartMsg>>,
|
|
|
|
|
/// Field tiles of the farms on this shard.
|
|
|
|
|
fields: HashSet<usize>,
|
|
|
|
|
/// The latest prices of every market in the cluster.
|
|
|
|
|
board: Vec<Quote>,
|
|
|
|
|
/// Ids of workplaces, markets and carts share the animal id sequence.
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
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>,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
workplaces: Vec<Workplace>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
markets: Vec<Market>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
carts: Vec<Cart>,
|
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);
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
person.coins = self.config.initial_person_coins;
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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_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(),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
workplace_actors: BTreeMap::new(),
|
|
|
|
|
market_actors: BTreeMap::new(),
|
|
|
|
|
cart_actors: BTreeMap::new(),
|
|
|
|
|
fields: HashSet::new(),
|
|
|
|
|
board: Vec::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();
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
w.found_economy();
|
|
|
|
|
let people = w.observe_people();
|
|
|
|
|
w.employ(&people);
|
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)
|
|
|
|
|
}
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
Arrival::Cart(id, actor) => {
|
|
|
|
|
self.cart_actors.insert(id, actor);
|
|
|
|
|
EntityKey::new(Kind::Cart, id)
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
};
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
for cart in &census.carts {
|
|
|
|
|
if !self.owns(cart.position())
|
|
|
|
|
&& let Some(actor) = self.cart_actors.remove(&cart.id)
|
|
|
|
|
{
|
|
|
|
|
let shard = self.shard.owner(cart.x, self.width);
|
|
|
|
|
actor.tell(CartMsg::Depart(request, shard), self.mailbox.reply_to());
|
|
|
|
|
leaving.push((cart.key(), shard, LocalActor::Cart(actor)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 17:20:30 +00:00
|
|
|
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),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
carts: self.observe_carts(),
|
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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
tools: 0,
|
|
|
|
|
coins: 0,
|
|
|
|
|
home: Some(self.home),
|
|
|
|
|
job: None,
|
|
|
|
|
cart: None,
|
|
|
|
|
unpaid_days: 0,
|
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();
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let new_day = self.tick.is_multiple_of(self.calendar.ticks_per_day);
|
|
|
|
|
if new_day {
|
|
|
|
|
self.grow_crops();
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
carts: self.observe_carts(),
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +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.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);
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
self.tick_carts(&mut census);
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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_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
|
|
|
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);
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
if new_day {
|
|
|
|
|
self.employ(&census.people);
|
|
|
|
|
self.refresh_board();
|
|
|
|
|
}
|
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();
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
for person in census.people.iter().filter(|person| person.dead) {
|
|
|
|
|
self.leave_job(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
|
|
|
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();
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
if self.res[i].0 != 0 || self.fields.contains(&i) {
|
2026-10-07 16:04:37 +00:00
|
|
|
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)?;
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let at_home = self.at_home(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
|
|
|
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
|
|
|
}
|
|
|
|
|
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
/// Harvests up to `room` of `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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
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
|
|
|
kind: u8,
|
|
|
|
|
target: (usize, usize),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
room: 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
|
|
|
) -> ((usize, usize), Goods) {
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let pos = person.position();
|
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
|
|
|
}
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let yield_n: u16 = if person.tools > 0 || self.tools >= self.population_u32() {
|
2026-10-11 19:23:50 +00:00
|
|
|
self.config.person_tooled_gather_yield
|
|
|
|
|
} else {
|
|
|
|
|
self.config.person_gather_yield
|
|
|
|
|
};
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let room = u16::try_from(room).unwrap_or(u16::MAX);
|
|
|
|
|
let take = yield_n.min(self.res[idx].1).min(room);
|
2026-10-11 19:23:50 +00:00
|
|
|
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();
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let home = self.home_of(p);
|
|
|
|
|
let at_home = self.at_home(p);
|
|
|
|
|
let activity = |action: &str, position| Activity::new(action, position, Goods::default());
|
|
|
|
|
let on_trip = p.job.as_ref().is_some_and(Job::on_trip);
|
|
|
|
|
let food_at_home = self.food > 0 || !self.owns(home);
|
|
|
|
|
if p.hunger > self.config.hunger_return_threshold && food_at_home && !at_home && !on_trip {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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()),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
employed: p.job.is_some(),
|
|
|
|
|
work_time: self.work_time(),
|
|
|
|
|
shift_done: p
|
|
|
|
|
.job
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|job| job.shift_done(self.today())),
|
|
|
|
|
on_shift: p.job.as_ref().is_some_and(Job::on_shift),
|
|
|
|
|
on_trip,
|
|
|
|
|
coins: i64::from(p.coins),
|
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) {
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
("work", ..) if p.job.is_some() => self.work(p, census),
|
|
|
|
|
("rest", ..) => activity("resting", pos),
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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)), _) => {
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let room = self.capacity(p).saturating_sub(p.load());
|
|
|
|
|
let (position, gathered) = self.gather(p, kind, target, room);
|
|
|
|
|
Activity::new(label, 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 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),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
workplaces: self.observe_workplaces(),
|
|
|
|
|
markets: self.observe_markets(),
|
|
|
|
|
carts: census.carts,
|
2026-10-08 06:44:30 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 18:29:11 +00:00
|
|
|
#[cfg(test)]
|
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
|
|
|
}
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
if saved.markets.is_empty() && saved.workplaces.is_empty() {
|
|
|
|
|
world.found_economy();
|
|
|
|
|
} else {
|
|
|
|
|
for market in saved.markets {
|
|
|
|
|
world.spawn_market_actor(market);
|
|
|
|
|
}
|
|
|
|
|
for workplace in saved.workplaces {
|
|
|
|
|
world.spawn_workplace_actor(workplace);
|
|
|
|
|
}
|
|
|
|
|
for cart in saved.carts {
|
|
|
|
|
world.spawn_cart_actor(cart);
|
|
|
|
|
}
|
|
|
|
|
world.refresh_board();
|
|
|
|
|
}
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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),
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
"coins" => PersonEdit::Coins(u),
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the 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.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!(
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
"[{},{},{},\"{}\",{:.0},{},{},{},\"{}\",{},{},{},{}]",
|
2026-10-11 15:48:26 +00:00
|
|
|
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
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
.map_or_else(|| "null".to_string(), |partner| partner.to_string()),
|
|
|
|
|
p.coins,
|
|
|
|
|
p.job.as_ref().map_or_else(
|
|
|
|
|
|| "null".to_string(),
|
|
|
|
|
|job| format!("\"{}\"", job.trade.key())
|
|
|
|
|
)
|
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
|
|
|
}
|
|
|
|
|
}
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
self.write_economy_json(s);
|
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")]
|
2026-10-12 18:29:11 +00:00
|
|
|
pub fn console(&self) -> picard::console::Console {
|
|
|
|
|
self.actor_system.console()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lets the Picard console reach the actors of another shard as `@name`.
|
|
|
|
|
#[cfg(feature = "console")]
|
|
|
|
|
pub fn register_console_peer(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
|
|
|
|
sender: picard::console::ConsolePeerSender,
|
|
|
|
|
query: picard::console::ConsolePeerQuery,
|
|
|
|
|
) {
|
|
|
|
|
self.actor_system.register_console_peer(name, sender);
|
|
|
|
|
self.actor_system.register_console_peer_query(name, query);
|
2026-10-12 10:43:05 +00:00
|
|
|
}
|
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.
|
|
|
|
|
impl World {
|
2026-10-12 18:29:11 +00:00
|
|
|
pub fn tick(&self) -> u64 {
|
|
|
|
|
self.tick
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Takes in entities handed over by other shards, so that a snapshot includes them.
|
|
|
|
|
pub fn admit_arrivals(&mut self) {
|
|
|
|
|
self.receive_arrivals();
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
pub fn shard(&self) -> Shard {
|
|
|
|
|
self.shard
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn router(&self) -> &Arc<Router> {
|
|
|
|
|
&self.router
|
|
|
|
|
}
|
|
|
|
|
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
/// Prices of this shard's markets, for the gateway to publish to every shard.
|
|
|
|
|
pub fn quotes(&self) -> Vec<Quote> {
|
|
|
|
|
let local: HashSet<EntityKey> = self
|
|
|
|
|
.market_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Market, id))
|
|
|
|
|
.collect();
|
|
|
|
|
self.board
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|quote| local.contains("e.market))
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Replaces the prices of other shards' markets with the ones the gateway published.
|
|
|
|
|
pub fn set_board(&mut self, quotes: Vec<Quote>) {
|
|
|
|
|
let local: HashSet<EntityKey> = self
|
|
|
|
|
.market_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Market, id))
|
|
|
|
|
.collect();
|
|
|
|
|
self.board.retain(|quote| local.contains("e.market));
|
|
|
|
|
self.board
|
|
|
|
|
.extend(quotes.into_iter().filter(|q| !local.contains(&q.market)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 17:20:30 +00:00
|
|
|
/// 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));
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
let workplaces = self
|
|
|
|
|
.workplace_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Workplace, id));
|
|
|
|
|
let markets = self
|
|
|
|
|
.market_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Market, id));
|
|
|
|
|
let carts = self
|
|
|
|
|
.cart_actors
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|&id| EntityKey::new(Kind::Cart, id));
|
|
|
|
|
people
|
|
|
|
|
.chain(rabbits)
|
|
|
|
|
.chain(wolves)
|
|
|
|
|
.chain(workplaces)
|
|
|
|
|
.chain(markets)
|
|
|
|
|
.chain(carts)
|
|
|
|
|
.collect()
|
2026-10-12 17:20:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 15:29:17 +00:00
|
|
|
impl Drop for World {
|
|
|
|
|
fn drop(&mut self) {
|
2026-10-11 18:42:29 +00:00
|
|
|
// Shutdown starts eagerly; Drop must not block waiting for completion.
|
|
|
|
|
drop(self.actor_system.shutdown());
|
2026-10-11 15:29:17 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 06:44:30 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-10-12 17:20:30 +00:00
|
|
|
use super::actors::{Activity, AnimalTurn, Goods, PersonEdit, PersonMsg, ReplyBody};
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
use super::economy::{Cart, Good, Job, MarketMsg, Phase, Trade, Workplace};
|
|
|
|
|
use super::router::Kind;
|
2026-10-12 17:20:30 +00:00
|
|
|
use super::{
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
Animal, CROP, DEFAULT_SCRIPT, EntityKey, FORWARDING_TICKS, H, LifeStage, Person, Router,
|
|
|
|
|
Shard, SimScript, W, World, cheb, default_config,
|
2026-10-12 17:20:30 +00:00
|
|
|
};
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
fn economy_script() -> SimScript {
|
|
|
|
|
let source = [
|
|
|
|
|
("WIDTH", "96"),
|
|
|
|
|
("HEIGHT", "64"),
|
|
|
|
|
("TICKS_PER_DAY", "240"),
|
|
|
|
|
("INITIAL_POPULATION", "30"),
|
|
|
|
|
("INITIAL_YOUNG_ADULTS", "30"),
|
|
|
|
|
("INITIAL_RABBITS", "200"),
|
|
|
|
|
("INITIAL_FOOD", "600"),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.fold(DEFAULT_SCRIPT.to_string(), |source, (name, value)| {
|
|
|
|
|
with_constant(&source, name, value)
|
|
|
|
|
});
|
|
|
|
|
SimScript::load(&source).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait_until(mut done: impl FnMut() -> bool, what: &str) {
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
|
|
|
while !done() {
|
|
|
|
|
assert!(Instant::now() < deadline, "{what} never happened");
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Moves the clock to the middle of the working hours of the current day.
|
|
|
|
|
fn set_work_time(world: &mut World) {
|
|
|
|
|
let day = world.calendar.ticks_per_day;
|
|
|
|
|
world.tick = world.tick / day * day + day * 45 / 100;
|
|
|
|
|
assert!(world.work_time());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A grown-up of `world` employed at `employer`, standing at `position` in `phase`.
|
|
|
|
|
fn worker(
|
|
|
|
|
world: &World,
|
|
|
|
|
employer: EntityKey,
|
|
|
|
|
phase: Phase,
|
|
|
|
|
position: (usize, usize),
|
|
|
|
|
) -> Person {
|
|
|
|
|
let markets = world.observe_markets();
|
|
|
|
|
let (trade, site, market) = match employer.kind {
|
|
|
|
|
Kind::Market => {
|
|
|
|
|
let market = markets.iter().find(|m| m.key() == employer).unwrap();
|
|
|
|
|
(Trade::Merchant, market.site(), market.clone())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
let workplace = world
|
|
|
|
|
.observe_workplaces()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|w| w.key() == employer)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let market = markets
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|m| m.key() == workplace.market)
|
|
|
|
|
.unwrap();
|
|
|
|
|
(workplace.trade, workplace.site(), market.clone())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut person = world
|
|
|
|
|
.observe_people()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|person| person.stage.can_partner() && person.job.is_none())
|
|
|
|
|
.unwrap_or_else(|| world.observe_people()[0].clone());
|
|
|
|
|
let mut job = Job::new(employer, trade, site, &market);
|
|
|
|
|
job.phase = phase;
|
|
|
|
|
person.job = Some(job);
|
|
|
|
|
(person.x, person.y) = position;
|
|
|
|
|
person.hunger = 0.0;
|
|
|
|
|
person
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn workplace_of(world: &World, trade: Trade) -> Workplace {
|
|
|
|
|
world
|
|
|
|
|
.observe_workplaces()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|workplace| workplace.trade == trade)
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_shard_founds_markets_and_workplaces_and_hires_staff() {
|
|
|
|
|
let world = World::new_with_script(42, economy_script());
|
|
|
|
|
let markets = world.observe_markets();
|
|
|
|
|
let workplaces = world.observe_workplaces();
|
|
|
|
|
assert_eq!(markets.len(), world.config.trade_merchant.count);
|
|
|
|
|
for trade in Trade::WORKSHOPS {
|
|
|
|
|
assert!(
|
|
|
|
|
workplaces.iter().any(|w| w.trade == trade),
|
|
|
|
|
"no {} was founded",
|
|
|
|
|
trade.key()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let farm = workplace_of(&world, Trade::Farm);
|
|
|
|
|
assert!(!farm.fields.is_empty());
|
|
|
|
|
for &(x, y) in &farm.fields {
|
|
|
|
|
assert_eq!(world.res[y * world.width + x].0, CROP);
|
|
|
|
|
}
|
|
|
|
|
let employed = world
|
|
|
|
|
.observe_people()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|person| person.job.is_some())
|
|
|
|
|
.count();
|
|
|
|
|
let staffed: usize = workplaces
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|w| w.staff.employees.len())
|
|
|
|
|
.chain(markets.iter().map(|m| m.staff.employees.len()))
|
|
|
|
|
.sum();
|
|
|
|
|
assert!(employed > 0);
|
|
|
|
|
assert_eq!(employed, staffed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn workers_finish_their_shifts_sell_goods_and_get_paid() {
|
|
|
|
|
let mut world = World::new_with_script(42, economy_script());
|
|
|
|
|
let start: u32 = world.observe_people().iter().map(|p| p.coins).sum();
|
|
|
|
|
let days = 3;
|
|
|
|
|
while world.tick < world.calendar.ticks_per_day * days {
|
|
|
|
|
world.step();
|
|
|
|
|
}
|
|
|
|
|
let people = world.observe_people();
|
|
|
|
|
assert!(
|
|
|
|
|
people
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|person| person.job.as_ref())
|
|
|
|
|
.any(|job| job.day.is_some()),
|
|
|
|
|
"nobody finished a shift: {:?}",
|
|
|
|
|
people
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|p| p.job.is_some())
|
|
|
|
|
.map(|p| (p.id, p.action.as_str(), p.job.as_ref().unwrap().phase))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
);
|
|
|
|
|
let stock: u32 = world
|
|
|
|
|
.observe_markets()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|market| market.stock.total())
|
|
|
|
|
.sum();
|
|
|
|
|
assert!(stock > 0, "no goods reached the markets");
|
|
|
|
|
let earned: u32 = people.iter().map(|p| p.coins).sum();
|
|
|
|
|
assert_ne!(earned, start, "nobody earned or spent anything");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn farmers_harvest_ripe_crops_and_sow_empty_fields() {
|
|
|
|
|
let mut world = World::new_with_script(42, economy_script());
|
|
|
|
|
set_work_time(&mut world);
|
|
|
|
|
let farm = workplace_of(&world, Trade::Farm);
|
|
|
|
|
for &(x, y) in &farm.fields {
|
|
|
|
|
world.res[y * world.width + x] = (CROP, 1);
|
|
|
|
|
}
|
|
|
|
|
let field = farm.fields[0];
|
|
|
|
|
let index = field.1 * world.width + field.0;
|
|
|
|
|
world.res[index] = (CROP, world.config.crop_ripe);
|
|
|
|
|
let mut farmer = worker(&world, farm.key(), Phase::Work, field);
|
|
|
|
|
let mut census = world.census();
|
|
|
|
|
|
|
|
|
|
let harvest = world.work(&farmer, &mut census);
|
|
|
|
|
assert_eq!(harvest.action, "harvesting");
|
|
|
|
|
assert_eq!(world.res[index], (0, 0));
|
|
|
|
|
let job = harvest.job.unwrap();
|
|
|
|
|
assert_eq!(job.goods.food, world.config.crop_yield);
|
|
|
|
|
assert_eq!(farmer.food, 0, "the harvest belongs to the farm");
|
|
|
|
|
|
|
|
|
|
farmer.job = Some(job);
|
|
|
|
|
let sow = world.work(&farmer, &mut census);
|
|
|
|
|
assert_eq!(sow.action, "sowing");
|
|
|
|
|
assert_eq!(world.res[index], (CROP, 1));
|
|
|
|
|
|
|
|
|
|
world.grow_crops();
|
|
|
|
|
assert_eq!(world.res[index], (CROP, 2));
|
|
|
|
|
world.regrow();
|
|
|
|
|
assert_eq!(world.res[index].0, CROP, "fields don't grow wild");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cartwrights_build_carts_that_others_buy_for_more_capacity() {
|
|
|
|
|
let mut world = World::new_with_script(42, economy_script());
|
|
|
|
|
set_work_time(&mut world);
|
|
|
|
|
let shop = workplace_of(&world, Trade::Cartwright);
|
|
|
|
|
let mut cartwright = worker(&world, shop.key(), Phase::Craft, shop.site());
|
|
|
|
|
cartwright.job.as_mut().unwrap().goods.wood = world.config.cart_wood_cost;
|
|
|
|
|
let mut census = world.census();
|
|
|
|
|
|
|
|
|
|
let built = world.work(&cartwright, &mut census);
|
|
|
|
|
assert_eq!(built.action, "building a cart");
|
|
|
|
|
let mut job = built.job.unwrap();
|
|
|
|
|
let [cart] = job.wares[..] else {
|
|
|
|
|
panic!("no cart was built: {job:?}");
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(job.goods.wood, 0);
|
|
|
|
|
let carts = world.observe_carts();
|
|
|
|
|
assert_eq!(carts.len(), 1);
|
|
|
|
|
assert_eq!(carts[0].owner, Some(cartwright.id));
|
|
|
|
|
|
|
|
|
|
job.phase = Phase::Sell;
|
|
|
|
|
cartwright.job = Some(job);
|
|
|
|
|
(cartwright.x, cartwright.y) = shop.market_site;
|
|
|
|
|
let sold = world.work(&cartwright, &mut census);
|
|
|
|
|
assert!(sold.job.unwrap().wares.is_empty());
|
|
|
|
|
let market = world.market_actors[&shop.market.id].clone();
|
|
|
|
|
wait_until(
|
|
|
|
|
|| {
|
|
|
|
|
world.observe_carts()[0].owner.is_none()
|
|
|
|
|
&& matches!(
|
|
|
|
|
world.ask(&market, MarketMsg::Observe),
|
|
|
|
|
ReplyBody::Market(m) if m.carts == vec![cart]
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
"the cart sale",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut buyer = worker(&world, shop.key(), Phase::Sell, shop.market_site);
|
|
|
|
|
buyer.id = world
|
|
|
|
|
.observe_people()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|p| p.id)
|
|
|
|
|
.find(|&id| id != cartwright.id)
|
|
|
|
|
.unwrap();
|
|
|
|
|
buyer.coins = 200;
|
|
|
|
|
buyer.tools = 1;
|
|
|
|
|
world.food = world.household_food_target();
|
|
|
|
|
let bought = world.work(&buyer, &mut census);
|
|
|
|
|
assert_eq!(bought.cart, Some(cart));
|
|
|
|
|
assert!(bought.spent > 0);
|
|
|
|
|
wait_until(
|
|
|
|
|
|| world.observe_carts()[0].owner == Some(buyer.id),
|
|
|
|
|
"the cart purchase",
|
|
|
|
|
);
|
|
|
|
|
let carrying = world.capacity(&buyer);
|
|
|
|
|
buyer.cart = bought.cart;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
world.capacity(&buyer),
|
|
|
|
|
carrying + world.config.cart_capacity
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn merchants_trade_at_markets_on_other_shards() {
|
|
|
|
|
let mut shards = connected_shards(2);
|
|
|
|
|
publish_directory(&mut shards);
|
|
|
|
|
let home_market = shards[0].observe_markets()[0].clone();
|
|
|
|
|
let far_market = shards[1].observe_markets()[0].clone();
|
|
|
|
|
// Food is scarce far away and plentiful at home.
|
|
|
|
|
let mut quotes = shards[0].quotes();
|
|
|
|
|
quotes.extend(shards[1].quotes());
|
|
|
|
|
for world in &mut shards {
|
|
|
|
|
world.set_board(quotes.clone());
|
|
|
|
|
for quote in &mut world.board {
|
|
|
|
|
if quote.market == home_market.key() {
|
|
|
|
|
quote.prices[Good::Food.index()] = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(shards[0].board.len(), 2);
|
|
|
|
|
assert_eq!(shards[0].quotes().len(), 1);
|
|
|
|
|
|
|
|
|
|
let mut merchant = worker(
|
|
|
|
|
&shards[0],
|
|
|
|
|
home_market.key(),
|
|
|
|
|
Phase::Commute,
|
|
|
|
|
home_market.site(),
|
|
|
|
|
);
|
|
|
|
|
merchant.job.as_mut().unwrap().goods.food = 6;
|
|
|
|
|
let mut census = shards[0].census();
|
|
|
|
|
let setting_out = shards[0].work(&merchant, &mut census);
|
|
|
|
|
let job = setting_out.job.unwrap();
|
|
|
|
|
assert_eq!(job.phase, Phase::Travel);
|
|
|
|
|
assert_eq!(job.destination, Some((far_market.key(), far_market.site())));
|
|
|
|
|
|
|
|
|
|
// At the far market the merchant is on the other shard, which drives them now.
|
|
|
|
|
merchant.job = Some(job);
|
|
|
|
|
(merchant.x, merchant.y) = far_market.site();
|
|
|
|
|
let mut census = shards[1].census();
|
|
|
|
|
let traded = shards[1].work(&merchant, &mut census);
|
|
|
|
|
let job = traded.job.unwrap();
|
|
|
|
|
assert_eq!(job.phase, Phase::Return);
|
|
|
|
|
assert_eq!(job.goods.food, 0);
|
|
|
|
|
assert!(job.coins > 0);
|
|
|
|
|
let far = shards[1].market_actors[&far_market.id].clone();
|
|
|
|
|
let ReplyBody::Market(far_now) = shards[1].ask(&far, MarketMsg::Observe) else {
|
|
|
|
|
panic!("market actor did not answer");
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(far_now.stock.food, 6);
|
|
|
|
|
|
|
|
|
|
// Back home, the takings settle with the home market on the first shard.
|
|
|
|
|
merchant.job = Some(job.clone());
|
|
|
|
|
(merchant.x, merchant.y) = home_market.site();
|
|
|
|
|
let mut census = shards[0].census();
|
|
|
|
|
let settled = shards[0].work(&merchant, &mut census);
|
|
|
|
|
assert_eq!(settled.job.unwrap().phase, Phase::Done);
|
|
|
|
|
let home = shards[0].market_actors[&home_market.id].clone();
|
|
|
|
|
wait_until(
|
|
|
|
|
|| {
|
|
|
|
|
matches!(
|
|
|
|
|
shards[0].ask(&home, MarketMsg::Observe),
|
|
|
|
|
ReplyBody::Market(m) if m.coins >= home_market.coins + job.coins - shards[0].config.merchant_escrow
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
"the merchant's settlement",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn markets_workplaces_and_carts_survive_snapshots() {
|
|
|
|
|
let mut world = World::new_with_script(42, economy_script());
|
|
|
|
|
let shop = workplace_of(&world, Trade::Cartwright);
|
|
|
|
|
world.spawn_cart_actor(Cart {
|
|
|
|
|
id: 99,
|
|
|
|
|
x: shop.x,
|
|
|
|
|
y: shop.y,
|
|
|
|
|
owner: None,
|
|
|
|
|
wear: 5,
|
|
|
|
|
broken: false,
|
|
|
|
|
});
|
|
|
|
|
let restored = World::from_snapshot_with_script(
|
|
|
|
|
&world.snapshot_json().unwrap(),
|
|
|
|
|
42,
|
|
|
|
|
economy_script(),
|
|
|
|
|
Shard::SINGLE,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
serde_json::to_value(restored.observe_markets()).unwrap(),
|
|
|
|
|
serde_json::to_value(world.observe_markets()).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
serde_json::to_value(restored.observe_workplaces()).unwrap(),
|
|
|
|
|
serde_json::to_value(world.observe_workplaces()).unwrap()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(restored.observe_carts()[0].wear, 5);
|
|
|
|
|
assert_eq!(restored.fields, world.fields);
|
|
|
|
|
let state: serde_json::Value = serde_json::from_str(&restored.state_json()).unwrap();
|
|
|
|
|
assert_eq!(state["carts"][0][0], 99);
|
|
|
|
|
assert!(state["markets"].as_array().is_some_and(|m| !m.is_empty()));
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
2026-10-12 18:29:11 +00:00
|
|
|
assert_ne!(super::regenerated_seed(world.seed()), world.seed());
|
2026-10-08 06:58:34 +00:00
|
|
|
}
|
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,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
Activity::new(
|
|
|
|
|
"foraging",
|
|
|
|
|
(3, 4),
|
|
|
|
|
Goods {
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
food: 2,
|
|
|
|
|
stone: 1,
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +00:00
|
|
|
..Goods::default()
|
refactor(world)!: make Picard actors own entity state
People, rabbits, wolves and partnerships are now Picard actors that own
their state and apply their own rules (ageing, hunger and meals,
starvation, energy, breeding costs, death, birth intervals). The world
no longer keeps mirrored Vecs that were read from and written back to
generic entity actors every step; it only reaches entities through
typed messages and replies, correlated by request id through a single
inbox actor.
World-level state (terrain, resources, stockpile, houses, calendar, the
world random stream) stays in the world, and behaviour decisions stay
on the simulation thread because the Rune VM is not Send. Replies are
processed in entity order and each person has its own persisted random
stream, so runs and snapshot restores stay deterministic.
Rabbits killed by hunters no longer move or breed later in the same
tick.
BREAKING CHANGE: Picard actors are named person-<id>, rabbit-<id>,
wolf-<id> and partnership-<id> instead of entity-<id>, which changes
the paths used with the actor console.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 14:47:44 +00:00
|
|
|
},
|
feat: jobs, markets, haggling, carts and merchants
Each shard founds markets and workplaces (farms, woodcutters, quarries,
hunters, toolmakers, cartwrights). Adults are hired, commute to work,
sell their goods at the market, get paid and go home. Markets, workplaces
and carts are actors; prices follow local supply and demand and trades
are haggled. Merchants trade with markets on other shards through the
router, so they do not know whether a market is remote.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:10:22 +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
|
|
|
);
|
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
|
|
|
}
|