feat(sim): integrate Picard actors for living entities

Give people and animals persistent Picard actors and document runtime boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-10-11 17:29:17 +02:00
parent 0fbe9d553d
commit 81581132ad
4 changed files with 626 additions and 6 deletions

420
Cargo.lock generated
View File

@ -29,6 +29,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "any_spawner"
version = "0.3.0"
@ -46,6 +55,21 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arc-swap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [
"rustversion",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "async-lock"
version = "3.4.2"
@ -197,6 +221,17 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "better_default"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "bitflags"
version = "2.13.2"
@ -255,6 +290,19 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "codee"
version = "0.3.5"
@ -288,11 +336,18 @@ version = "0.15.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e9fbc53afcec54441422f1c596cd054655b32e70e1cbac956651b7b175a404"
dependencies = [
"async-trait",
"convert_case 0.6.0",
"json5",
"pathdiff",
"ron",
"rust-ini",
"serde-untagged",
"serde_core",
"serde_json",
"toml",
"winnow",
"yaml-rust2",
]
[[package]]
@ -311,6 +366,26 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "const-str"
version = "1.1.0"
@ -371,6 +446,18 @@ dependencies = [
"convert_case 0.11.0",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -401,6 +488,15 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
@ -435,6 +531,12 @@ version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@ -445,6 +547,20 @@ dependencies = [
"typenum",
]
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.11.1"
@ -496,6 +612,15 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@ -527,6 +652,20 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "encoding_rs"
version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
dependencies = [
"cfg-if",
"core_detect",
"multiversion_no_op",
"rustversion",
"scopeguard",
"simdutf8",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -539,6 +678,26 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472"
[[package]]
name = "erased-serde"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
dependencies = [
"serde",
]
[[package]]
name = "erased-serde"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
dependencies = [
"serde",
"serde_core",
"typeid",
]
[[package]]
name = "etcetera"
version = "0.8.0"
@ -756,9 +915,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
@ -837,6 +998,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@ -874,6 +1041,15 @@ dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "hashlink"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "heck"
version = "0.5.0"
@ -1011,6 +1187,30 @@ dependencies = [
"tower-service",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.3.0"
@ -1187,6 +1387,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c"
dependencies = [
"serde",
"ucd-trie",
]
[[package]]
name = "konst"
version = "0.2.20"
@ -1492,6 +1702,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]]
name = "musli"
version = "0.0.124"
@ -1654,6 +1870,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -1710,6 +1936,41 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "picard"
version = "0.4.2"
source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5"
dependencies = [
"better_default",
"chrono",
"config",
"crossbeam-channel",
"dashmap",
"futures",
"getrandom 0.3.4",
"gloo-timers",
"picard-macros",
"rand 0.9.5",
"serde",
"slog",
"slog-scope",
"slog-stdlog",
"thiserror 2.0.21",
"uuid",
"wasm-bindgen-futures",
"web-time",
]
[[package]]
name = "picard-macros"
version = "0.2.0"
source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "pin-project"
version = "1.1.13"
@ -2071,6 +2332,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ron"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3"
dependencies = [
"bitflags",
"once_cell",
"serde",
"serde_derive",
"typeid",
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.10"
@ -2198,6 +2473,16 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-demangle"
version = "0.1.28"
@ -2311,6 +2596,18 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-untagged"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
dependencies = [
"erased-serde 0.4.10",
"serde",
"serde_core",
"typeid",
]
[[package]]
name = "serde_core"
version = "1.0.229"
@ -2498,6 +2795,7 @@ version = "0.1.0"
dependencies = [
"axum",
"include_dir",
"picard",
"rand 0.8.8",
"rune",
"serde",
@ -2519,6 +2817,40 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slog"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1"
dependencies = [
"anyhow",
"erased-serde 0.3.31",
"rustversion",
"serde_core",
]
[[package]]
name = "slog-scope"
version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b76cf645c92e7850d5a1c9205ebf2864bd32c0ab3e978e6daad51fedf7ef54"
dependencies = [
"arc-swap",
"lazy_static",
"slog",
]
[[package]]
name = "slog-stdlog"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e"
dependencies = [
"log",
"slog",
"slog-scope",
]
[[package]]
name = "slotmap"
version = "1.1.1"
@ -2596,7 +2928,7 @@ dependencies = [
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"hashlink 0.10.0",
"indexmap",
"log",
"memchr",
@ -2921,6 +3253,15 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinystr"
version = "0.8.4"
@ -3157,12 +3498,24 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "ui"
version = "0.1.0"
@ -3479,6 +3832,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
@ -3516,6 +3879,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@ -3531,6 +3929,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -3706,6 +4113,17 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7"
[[package]]
name = "yaml-rust2"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b36710ce3a279cfce8465dbab826f161675a262950b922cb2c3663852dfe9eb0"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink 0.11.1",
]
[[package]]
name = "yoke"
version = "0.8.3"

View File

@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "postgres"] }
rand = "0.8"
picard = { git = "https://github.com/picard-rs/picard", rev = "50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" }
[build-dependencies]
wasm-bindgen-cli-support = "=0.2.129"

View File

@ -2,6 +2,8 @@
A browser-based world simulation with a Rust server and a Leptos/Wasm UI. The server runs the simulation and exposes its state over HTTP and WebSockets. You can edit terrain and resources, manage people and animals, and customize agent behavior with the included Rune script.
Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`.
The UI is built automatically as part of `cargo run` and embedded in the server binary. There is no separate manual UI build step.
## Getting Started

View File

@ -1,8 +1,17 @@
use picard::actors::{
Actor, ActorFactoryArgs, ActorRef, ActorRefFactory, ActorReference, ActorSystem,
Context as ActorContext, Sender, Tell,
};
use rune::termcolor::Buffer;
use rune::{Any, Context, Diagnostics, Module, Source, Sources, Vm};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::sync::Arc;
use std::sync::{
Arc,
mpsc::{SyncSender, sync_channel},
};
use std::time::Duration;
pub const W: usize = 96;
pub const H: usize = 64;
@ -148,8 +157,10 @@ fn load_script(src: &str) -> Result<Vm, String> {
Ok(vm)
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Person {
#[serde(skip)]
actor_id: u64,
id: u32,
x: usize,
y: usize,
@ -167,14 +178,61 @@ impl Person {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Animal {
#[serde(skip)]
actor_id: u64,
x: usize,
y: usize,
energy: f32,
dead: bool,
}
#[derive(Clone, Debug)]
enum EntityKind {
Person(Person),
Rabbit(Animal),
Wolf(Animal),
}
#[derive(Clone, Debug)]
struct EntityState {
id: u64,
kind: EntityKind,
}
#[derive(Debug)]
enum EntityMessage {
Read(SyncSender<EntityState>),
Write(EntityState, SyncSender<()>),
}
struct EntityActor {
state: EntityState,
}
impl ActorFactoryArgs<EntityState> for EntityActor {
fn create_args(state: EntityState) -> Self {
Self { state }
}
}
impl Actor for EntityActor {
type Msg = EntityMessage;
fn recv(&mut self, _ctx: &ActorContext<Self::Msg>, msg: Self::Msg, _sender: Sender) {
match msg {
EntityMessage::Read(reply) => {
let _ = reply.send(self.state.clone());
}
EntityMessage::Write(state, reply) => {
self.state = state;
let _ = reply.send(());
}
}
}
}
pub struct World {
seed: u64,
tiles: Vec<u8>,
@ -182,6 +240,9 @@ pub struct World {
people: Vec<Person>,
rabbits: Vec<Animal>,
wolves: Vec<Animal>,
actor_system: ActorSystem,
entities: HashMap<u64, ActorRef<EntityMessage>>,
next_actor_id: u64,
houses: Vec<(usize, usize)>,
home: (usize, usize),
food: u32,
@ -260,6 +321,9 @@ impl World {
people: vec![],
rabbits: vec![],
wolves: vec![],
actor_system: ActorSystem::new().expect("failed to start Picard actor system"),
entities: HashMap::new(),
next_actor_id: 1,
houses: vec![],
home: (W / 2, H / 2),
food: 4,
@ -296,6 +360,7 @@ impl World {
for _ in 0..60 {
if let Some(p) = w.random_land() {
w.rabbits.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
@ -307,6 +372,7 @@ impl World {
if let Some(p) = w.random_land() {
if cheb(p, w.home) > 15 {
w.wolves.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
@ -315,9 +381,104 @@ impl World {
}
}
}
w.sync_entities();
w
}
fn read_entities(actors: &HashMap<u64, ActorRef<EntityMessage>>) -> Vec<EntityState> {
let replies: Vec<_> = actors
.values()
.map(|actor| {
let (reply, result) = sync_channel(1);
actor.tell(EntityMessage::Read(reply), None);
result
})
.collect();
let mut states: Vec<_> = replies
.into_iter()
.map(|reply| {
reply
.recv_timeout(Duration::from_secs(5))
.expect("Picard entity actor did not respond to read")
})
.collect();
states.sort_by_key(|state| state.id);
states
}
fn load_entities(&mut self) {
let states = Self::read_entities(&self.entities);
self.people.clear();
self.rabbits.clear();
self.wolves.clear();
for state in states {
match state.kind {
EntityKind::Person(person) => self.people.push(person),
EntityKind::Rabbit(animal) => self.rabbits.push(animal),
EntityKind::Wolf(animal) => self.wolves.push(animal),
}
}
}
fn sync_entities(&mut self) {
for person in &mut self.people {
if person.actor_id == 0 {
person.actor_id = self.next_actor_id;
self.next_actor_id += 1;
}
}
for animal in self.rabbits.iter_mut().chain(self.wolves.iter_mut()) {
if animal.actor_id == 0 {
animal.actor_id = self.next_actor_id;
self.next_actor_id += 1;
}
}
let states = self
.people
.iter()
.cloned()
.map(|person| EntityState {
id: person.actor_id,
kind: EntityKind::Person(person),
})
.chain(self.rabbits.iter().cloned().map(|animal| EntityState {
id: animal.actor_id,
kind: EntityKind::Rabbit(animal),
}))
.chain(self.wolves.iter().cloned().map(|animal| EntityState {
id: animal.actor_id,
kind: EntityKind::Wolf(animal),
}));
let mut active = HashSet::new();
for state in states {
active.insert(state.id);
if let Some(actor) = self.entities.get(&state.id) {
let (reply, result) = sync_channel(1);
actor.tell(EntityMessage::Write(state, reply), None);
result
.recv_timeout(Duration::from_secs(5))
.expect("Picard entity actor did not respond to write");
} else {
let id = state.id;
let name = format!("entity-{id}");
let actor = self
.actor_system
.actor_of_args::<EntityActor, _>(&name, state)
.expect("failed to create Picard entity actor");
self.entities.insert(id, actor);
}
}
self.entities.retain(|id, actor| {
if active.contains(id) {
true
} else {
actor.stop();
false
}
});
}
fn find_land(&self, kind: u8) -> Option<(usize, usize)> {
let c = (W / 2, H / 2);
let mut best: Option<(usize, usize)> = None;
@ -346,6 +507,7 @@ impl World {
fn spawn_person(&mut self) {
let (x, y) = self.home;
self.people.push(Person {
actor_id: 0,
id: self.next_id,
x,
y,
@ -436,6 +598,7 @@ impl World {
}
pub fn step(&mut self) {
self.load_entities();
self.tick += 1;
self.regrow();
if self.tick % 2 == 0 {
@ -454,6 +617,7 @@ impl World {
self.rabbits.retain(|a| !a.dead);
self.wolves.retain(|a| !a.dead);
self.population();
self.sync_entities();
}
fn regrow(&mut self) {
@ -483,6 +647,7 @@ impl World {
if self.rabbits.len() < 15 {
if let Some(p) = self.random_land() {
self.rabbits.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
@ -494,6 +659,7 @@ impl World {
if let Some(p) = self.random_land() {
if cheb(p, self.home) > 15 {
self.wolves.push(Animal {
actor_id: 0,
x: p.0,
y: p.1,
energy: 70.0,
@ -677,6 +843,7 @@ impl World {
if a.energy > 80.0 && self.rabbits.len() < 120 && self.rng.chance(0.01) {
a.energy -= 30.0;
self.rabbits.push(Animal {
actor_id: 0,
x: a.x,
y: a.y,
energy: 50.0,
@ -715,6 +882,7 @@ impl World {
if a.energy > 90.0 && self.wolves.len() < 10 && self.rng.chance(0.02) {
a.energy -= 40.0;
self.wolves.push(Animal {
actor_id: 0,
x: a.x,
y: a.y,
energy: 50.0,
@ -800,13 +968,17 @@ impl World {
return Err("world snapshot has invalid terrain dimensions".into());
}
let vm = load_script(&saved.script)?;
Ok(Self {
let mut world = Self {
seed: saved.seed.unwrap_or(fallback_seed),
tiles: saved.tiles,
res: saved.res,
people: saved.people,
rabbits: saved.rabbits,
wolves: saved.wolves,
actor_system: ActorSystem::new()
.map_err(|e| format!("failed to start Picard actor system: {e}"))?,
entities: HashMap::new(),
next_actor_id: 1,
houses: saved.houses,
home: saved.home,
food: saved.food,
@ -822,7 +994,9 @@ impl World {
paused: saved.paused,
speed: saved.speed,
terrain_rev: saved.terrain_rev,
})
};
world.sync_entities();
Ok(world)
}
fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> {
@ -838,6 +1012,7 @@ impl World {
/// Applies one whitespace-separated edit command; malformed commands are ignored.
pub fn apply(&mut self, cmd: &str) {
self.load_entities();
let t: Vec<&str> = cmd.split_whitespace().collect();
let num = |i: usize| t.get(i).and_then(|s| s.parse::<i64>().ok());
match t.first().copied() {
@ -923,6 +1098,7 @@ impl World {
}
left -= 1;
let a = Animal {
actor_id: 0,
x,
y,
energy: 70.0,
@ -985,6 +1161,7 @@ impl World {
}
_ => {}
}
self.sync_entities();
}
fn paint_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, kind: u8) {
@ -1093,6 +1270,12 @@ impl World {
}
}
impl Drop for World {
fn drop(&mut self) {
let _ = self.actor_system.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::{DAY, DEFAULT_SCRIPT, World};
@ -1136,4 +1319,20 @@ mod tests {
let world = World::new(42);
assert_ne!(world.regenerated_seed(), world.seed());
}
#[test]
fn picard_actors_own_living_entity_state() {
let mut world = World::new(42);
let person_id = world.people[0].id;
world.apply(&format!("person {person_id} hunger 73"));
world.people[0].hunger = 1.0;
world.load_entities();
assert_eq!(world.people[0].hunger, 73.0);
assert_eq!(
world.entities.len(),
world.people.len() + world.rabbits.len() + world.wolves.len()
);
}
}