feat: drive agent behavior with Rune scripts

This commit is contained in:
Steve Biedermann 2026-10-07 18:42:59 +02:00
parent 5760d097f7
commit dd02a16d36
6 changed files with 1368 additions and 119 deletions

699
Cargo.lock generated
View File

@ -2,37 +2,604 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "ascii"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "cc"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generator"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "lazy_static"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "musli"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b310b280353d9e1c92861820321f8742b02666acaf984a29cd8946965444384"
dependencies = [
"loom",
"musli-core",
"serde",
"simdutf8",
]
[[package]]
name = "musli-core"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00e227a374e92550ce2eb5002ae116e02a43926d7243c95997138406ae4e157"
dependencies = [
"musli-macros",
]
[[package]]
name = "musli-macros"
version = "0.0.124"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7427c9aa85c882cd4dbe712d2fcdc511db05d595f7787e6747c90cd7d67efc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "pin-project"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rune"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a577a0dc4b3e4cfe89247325b14b5f30096ddc82fa51fffb1d823fbc85b410"
dependencies = [
"anyhow",
"codespan-reporting",
"futures-core",
"futures-util",
"itoa",
"memchr",
"musli",
"num",
"once_cell",
"pin-project",
"rune-alloc",
"rune-core",
"rune-macros",
"rune-tracing",
"ryu",
"serde",
"syntree",
"unicode-ident",
]
[[package]]
name = "rune-alloc"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702abcb136e7e4ff61c29006f018ab535728388b950bfbd50a0a66d7a2db1ff6"
dependencies = [
"ahash",
"pin-project",
"rune-alloc-macros",
"serde",
]
[[package]]
name = "rune-alloc-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98fa2ddc4ebb390a257f05659081e5f4a1419ca65f0c7a3f4b02547860dd93f4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "rune-core"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fab02420e2189ad2d8d9efceca5319782873eb547c61b68732746cc161547aa"
dependencies = [
"musli",
"rune-alloc",
"serde",
"twox-hash",
]
[[package]]
name = "rune-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d314f34d8b30569189d90ac0f96eeedf629492c10d18146b40cfb76bd3fd2d52"
dependencies = [
"proc-macro2",
"quote",
"rune-core",
"syn 2.0.119",
]
[[package]]
name = "rune-tracing"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5b903935dfcb0ab9eec395f4c2e04b174448bb8d1fd35ef5181ad67140239a9"
dependencies = [
"rune-tracing-macros",
]
[[package]]
name = "rune-tracing-macros"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75193c702bddd8a5b74b1f9a5a992cef3c6b2df2909d6f0bb0f4b0f3ec8a2f66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "sim"
version = "0.1.0"
dependencies = [
"rune",
"tiny_http",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syntree"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c99c9cda412afe293a6b962af651b4594161ba88c1affe7ef66459ea040a06"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
@ -44,3 +611,135 @@ dependencies = [
"httpdate",
"log",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "twox-hash"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]

View File

@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024"
[dependencies]
rune = "0.14.2"
tiny_http = "0.12"

44
src/behavior.rn Normal file
View File

@ -0,0 +1,44 @@
// Behaviour scripts. Each function receives what the entity senses and
// returns the name of an action; the Rust side carries it out.
// person: sleep | go_home | eat | haul | hunt | forage | chop | mine | wander
pub fn person(c) {
if c.night {
return if c.at_home { "sleep" } else { "go_home" };
}
if c.hunger > 50.0 && c.carried_food == 0 && c.stock_food > 0 {
return "eat";
}
if c.carry >= 6 {
return "haul";
}
if c.stock_food < 12 || c.hunger > 50.0 {
if c.rabbit_near && (!c.berry_near || c.roll < 0.3) {
return "hunt";
}
if c.berry_near {
return "forage";
}
}
if c.stock_wood <= c.stock_stone * 2 + 8 {
if c.tree_near { "chop" } else if c.stone_near { "mine" } else { "wander" }
} else {
if c.stone_near { "mine" } else if c.tree_near { "chop" } else { "wander" }
}
}
// rabbit: flee | wander | stay
pub fn rabbit(c) {
if c.wolf_near {
if c.roll < 0.8 { "flee" } else { "stay" }
} else if c.roll < 0.5 {
"wander"
} else {
"stay"
}
}
// wolf: hunt | wander
pub fn wolf(c) {
if c.prey_near { "hunt" } else { "wander" }
}

View File

@ -14,11 +14,31 @@
}
#side {
width: 280px;
width: 340px;
height: 100vh;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
#side input[type=number] {
width: 56px;
}
#script {
width: 100%;
height: 260px;
box-sizing: border-box;
background: #181818;
color: #ddd;
font: 12px monospace;
}
#scriptErr {
color: #f77;
white-space: pre-wrap;
}
canvas {
display: block;
}
@ -40,8 +60,39 @@
<div id="side">
<h3>World</h3>
<div id="hud"></div>
<div>
<button id="pause">Pause</button>
Speed <select id="speed">
<option>1</option>
<option>2</option>
<option>4</option>
<option>8</option>
</select>
</div>
<h3>Tool</h3>
<div>
<select id="tool">
<option value="inspect">Inspect</option>
<option value="terrain">Paint terrain</option>
<option value="res">Place resource</option>
<option value="spawn">Spawn</option>
<option value="remove">Remove</option>
<option value="house">Add house</option>
<option value="energy">Set animal energy</option>
</select>
<select id="param"></select>
<input id="amount" type="number" value="5" min="1" max="100" title="amount / energy">
</div>
<div id="toolHelp" style="color:#888"></div>
<h3>Selected</h3>
<div id="hover">click a tile or drag to select a region</div>
<div id="editor"></div>
<h3>Stockpile</h3>
<div id="stockEdit"></div>
<h3>Behaviour script (Rune)</h3>
<textarea id="script" spellcheck="false"></textarea>
<div><button id="applyScript">Apply script</button></div>
<div id="scriptErr"></div>
<h3>Events</h3>
<div id="log"></div>
<h3>Legend</h3>
@ -53,11 +104,12 @@
const COLORS = ['#10305e', '#2a62a8', '#e0d08a', '#6aaa4a', '#3f7d3a', '#8a8a6a', '#aaaaaa'];
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
let terrain = null, off = null, state = null;
let terrain = null, off = null, state = null, terrainRev = -1;
async function loadTerrain() {
const t = await (await fetch('/api/terrain')).json();
terrain = t;
terrainRev = state ? state.rev : -1;
cv.width = t.w * T; cv.height = t.h * T;
off = document.createElement('canvas');
off.width = cv.width; off.height = cv.height;
@ -105,6 +157,8 @@
async function poll() {
try {
state = await (await fetch('/api/state')).json();
if (terrain && state.rev !== terrainRev) await loadTerrain();
syncControls();
const s = state.stock, h = Math.floor(state.hour), m = Math.floor((state.hour % 1) * 60);
document.getElementById('hud').innerHTML =
`Day ${state.day}, ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}<br>` +
@ -183,7 +237,92 @@
sel.x1 = t.x; sel.y1 = t.y;
draw(); showSelection();
});
window.addEventListener('mouseup', () => { dragging = false; });
window.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
const cmd = toolCommand();
if (cmd) post('/api/edit', cmd);
buildEditor();
});
const $ = id => document.getElementById(id);
const post = (url, body) => fetch(url, { method: 'POST', body });
const TOOLS = {
inspect: { help: 'Click or drag to inspect and edit.', opts: null },
terrain: { help: 'Drag a region to paint it.', opts: TERRAIN.map((n, i) => [i, n]) },
res: { help: 'Drag a region; amount sets units per tile.', opts: [[1, 'Tree'], [2, 'Berry bush'], [3, 'Stone'], [0, 'Clear']] },
spawn: { help: 'Drag a region; one per walkable tile (max 25).', opts: [['person', 'Person'], ['rabbit', 'Rabbit'], ['wolf', 'Wolf']] },
remove: { help: 'Drag a region to remove things in it.', opts: [['all', 'Everything'], ['people', 'People'], ['rabbits', 'Rabbits'], ['wolves', 'Wolves'], ['houses', 'Houses'], ['res', 'Resources']] },
house: { help: 'Drag a region to add houses.', opts: null },
energy: { help: 'Drag a region; amount sets animal energy (1-100).', opts: null },
};
function toolCommand() {
const tool = $('tool').value;
if (tool === 'inspect' || !sel) return null;
const [x0, y0, x1, y1] = selRect();
const r = `${x0} ${y0} ${x1} ${y1}`, p = $('param').value, a = parseInt($('amount').value) || 1;
switch (tool) {
case 'terrain': return `terrain ${r} ${p}`;
case 'res': return `res ${r} ${p} ${a}`;
case 'spawn': return `spawn ${p} ${r}`;
case 'remove': return `remove ${r} ${p}`;
case 'house': return `house ${r}`;
case 'energy': return `energy ${r} ${a}`;
}
}
$('tool').addEventListener('change', () => {
const t = TOOLS[$('tool').value];
$('param').innerHTML = (t.opts || []).map(([v, n]) => `<option value="${v}">${n}</option>`).join('');
$('param').style.display = t.opts ? '' : 'none';
$('toolHelp').textContent = t.help;
});
$('tool').dispatchEvent(new Event('change'));
$('pause').addEventListener('click', () => post('/api/edit', `pause ${state && state.paused ? 0 : 1}`));
$('speed').addEventListener('change', () => post('/api/edit', `speed ${$('speed').value}`));
let stockBuilt = false;
function syncControls() {
$('pause').textContent = state.paused ? 'Resume' : 'Pause';
if (document.activeElement !== $('speed')) $('speed').value = state.speed;
if (!stockBuilt) {
stockBuilt = true;
$('stockEdit').innerHTML = ['food', 'wood', 'stone', 'tools'].map(k =>
`${k} <input type="number" id="stock_${k}" min="0" value="${state.stock[k]}"> `).join('') +
'<button id="stockSet">Set</button>';
$('stockSet').addEventListener('click', () => {
for (const k of ['food', 'wood', 'stone', 'tools'])
post('/api/edit', `stock ${k} ${parseInt($('stock_' + k).value) || 0}`);
});
}
}
function buildEditor() {
const el = $('editor');
el.innerHTML = '';
if (!sel || !state) return;
const [x0, y0, x1, y1] = selRect();
const people = state.people.filter(p => p[1] >= x0 && p[1] <= x1 && p[2] >= y0 && p[2] <= y1).slice(0, 5);
for (const p of people) {
const row = document.createElement('div');
row.innerHTML = `Edit #${p[0]}: ` + [['hunger', p[4]], ['food', p[5]], ['wood', p[6]], ['stone', p[7]]].map(([k, v]) =>
`${k} <input type="number" min="0" max="99" data-k="${k}" value="${v}">`).join(' ') + ' <button>Set</button>';
row.querySelector('button').addEventListener('click', () => {
for (const i of row.querySelectorAll('input'))
post('/api/edit', `person ${p[0]} ${i.dataset.k} ${parseInt(i.value) || 0}`);
});
el.appendChild(row);
}
}
fetch('/api/script').then(r => r.text()).then(t => { $('script').value = t; });
$('applyScript').addEventListener('click', async () => {
const r = await post('/api/script', $('script').value);
$('scriptErr').textContent = r.ok ? 'Script applied.' : await r.text();
});
loadTerrain().then(poll);
</script>

View File

@ -1,26 +1,63 @@
mod world;
use std::io::Read;
use std::sync::mpsc::{self, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tiny_http::{Header, Response, Server};
use world::World;
use tiny_http::{Header, Method, Response, Server};
use world::{DEFAULT_SCRIPT, World};
const INDEX: &str = include_str!("index.html");
enum Msg {
Edit(String),
Script(String, Sender<Result<(), String>>),
}
fn main() {
let seed = std::env::args()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(42u64);
let addr = std::env::var("SIM_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".into());
let world = Arc::new(Mutex::new(World::new(seed)));
// The Rune VM is not Send, so the world lives on the sim thread and publishes JSON snapshots.
let terrain = Arc::new(Mutex::new(String::new()));
let state = Arc::new(Mutex::new(String::new()));
let script = Arc::new(Mutex::new(DEFAULT_SCRIPT.to_string()));
let (tx, rx) = mpsc::channel::<Msg>();
{
let world = Arc::clone(&world);
let (terrain, state, script) = (
Arc::clone(&terrain),
Arc::clone(&state),
Arc::clone(&script),
);
thread::spawn(move || {
let mut world = World::new(seed);
let mut rev = u32::MAX;
loop {
world.lock().unwrap().step();
while let Ok(msg) = rx.try_recv() {
match msg {
Msg::Edit(cmd) => world.apply(&cmd),
Msg::Script(src, reply) => {
let r = world.set_script(&src);
if r.is_ok() {
*script.lock().unwrap() = src;
}
let _ = reply.send(r);
}
}
}
if !world.paused {
for _ in 0..world.speed {
world.step();
}
}
if world.terrain_rev != rev {
rev = world.terrain_rev;
*terrain.lock().unwrap() = world.terrain_json();
}
*state.lock().unwrap() = world.state_json();
thread::sleep(Duration::from_millis(100));
}
});
@ -28,20 +65,51 @@ fn main() {
let server = Server::http(&addr).expect("failed to bind");
println!("Simulation running at http://{addr}");
for req in server.incoming_requests() {
let world = Arc::clone(&world);
for mut req in server.incoming_requests() {
let (terrain, state, script) = (
Arc::clone(&terrain),
Arc::clone(&state),
Arc::clone(&script),
);
let tx = tx.clone();
thread::spawn(move || {
let (body, ctype) = match req.url() {
"/" => (INDEX.to_string(), "text/html; charset=utf-8"),
"/api/terrain" => (world.lock().unwrap().terrain_json(), "application/json"),
"/api/state" => (world.lock().unwrap().state_json(), "application/json"),
_ => {
let _ = req.respond(Response::from_string("not found").with_status_code(404));
return;
let post = *req.method() == Method::Post;
let mut input = String::new();
if post {
let _ = req.as_reader().take(65536).read_to_string(&mut input);
}
let (status, body, ctype) = match (post, req.url()) {
(false, "/") => (200, INDEX.to_string(), "text/html; charset=utf-8"),
(false, "/api/terrain") => {
(200, terrain.lock().unwrap().clone(), "application/json")
}
(false, "/api/state") => (200, state.lock().unwrap().clone(), "application/json"),
(false, "/api/script") => (
200,
script.lock().unwrap().clone(),
"text/plain; charset=utf-8",
),
(true, "/api/edit") if input.len() <= 256 => {
let _ = tx.send(Msg::Edit(input));
(200, "ok".to_string(), "text/plain")
}
(true, "/api/script") => {
let (rtx, rrx) = mpsc::channel();
let _ = tx.send(Msg::Script(input, rtx));
match rrx.recv_timeout(Duration::from_secs(5)) {
Ok(Ok(())) => (200, "ok".to_string(), "text/plain"),
Ok(Err(e)) => (400, e, "text/plain; charset=utf-8"),
Err(_) => (503, "timed out".to_string(), "text/plain"),
}
}
_ => (404, "not found".to_string(), "text/plain"),
};
let header = Header::from_bytes("Content-Type", ctype).unwrap();
let _ = req.respond(Response::from_string(body).with_header(header));
let _ = req.respond(
Response::from_string(body)
.with_status_code(status)
.with_header(header),
);
});
}
}

View File

@ -1,4 +1,7 @@
use rune::termcolor::Buffer;
use rune::{Any, Context, Diagnostics, Module, Source, Sources, Vm};
use std::fmt::Write;
use std::sync::Arc;
pub const W: usize = 96;
pub const H: usize = 64;
@ -73,6 +76,77 @@ fn fbm(x: f32, y: f32, seed: u64) -> f32 {
sum / norm
}
/// What an entity senses this tick; passed to the Rune behaviour scripts.
#[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,
}
pub const DEFAULT_SCRIPT: &str = include_str!("behavior.rn");
/// Compiles the script and smoke-tests every entry point so errors surface at load time.
fn load_script(src: &str) -> Result<Vm, 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();
sources
.insert(Source::new("behavior", src).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();
let unit = match unit {
Ok(u) => u,
Err(_) => {
let mut buf = Buffer::no_color();
let _ = diagnostics.emit(&mut buf, &sources);
return Err(String::from_utf8_lossy(buf.as_slice()).into_owned());
}
};
let runtime = context.runtime().map_err(|e| e.to_string())?;
let mut vm = Vm::new(Arc::new(runtime), Arc::new(unit));
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(vm)
}
#[derive(Clone)]
pub struct Person {
id: u32,
@ -116,6 +190,10 @@ pub struct World {
tick: u64,
next_id: u32,
log: Vec<String>,
vm: Vm,
pub paused: bool,
pub speed: u32,
pub terrain_rev: u32,
}
fn cheb(a: (usize, usize), b: (usize, usize)) -> usize {
@ -163,6 +241,10 @@ impl World {
tick: DAY / 4,
next_id: 1,
log: vec![],
vm: load_script(DEFAULT_SCRIPT).expect("behavior.rn failed to load"),
paused: false,
speed: 1,
terrain_rev: 0,
};
w.home = w
.find_land(GRASS)
@ -393,11 +475,22 @@ impl World {
}
}
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()
}
}
}
fn tick_person(&mut self, i: usize) {
let mut p = self.people[i].clone();
let night = self.daylight() < 0.2;
let pos = (p.x, p.y);
let at_home = cheb(pos, self.home) <= 1;
let home = self.home;
let at_home = cheb(pos, home) <= 1;
p.hunger += if night && at_home { 0.04 } else { 0.1 };
if p.hunger >= 100.0 {
p.dead = true;
@ -416,6 +509,97 @@ impl World {
self.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
self.craft_and_build();
}
if p.hunger > 50.0 && p.food > 0 {
p.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
let berry = self.nearest_res(pos, BERRY, 25);
let tree = self.nearest_res(pos, TREE, 25);
let stone = self.nearest_res(pos, STONE, 25);
let rabbit = Self::nearest_animal(&self.rabbits, pos, 25);
let c = Ctx {
night,
at_home,
hunger: p.hunger as f64,
carry: p.carry() as i64,
carried_food: p.food as i64,
stock_food: self.food as i64,
stock_wood: self.wood as i64,
stock_stone: self.stone as i64,
berry_near: berry.is_some(),
tree_near: tree.is_some(),
stone_near: stone.is_some(),
rabbit_near: rabbit.is_some(),
roll: self.rng.f() as f64,
..Default::default()
};
let choice = self.decide("person", c);
let yield_n: u16 = if self.tools >= self.people.len() as u32 {
2
} else {
1
};
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) {
("sleep", ..) => p.action = "sleeping",
("go_home" | "haul" | "eat", ..) => {
p.action = match choice.as_str() {
"go_home" => "going home",
"haul" => "hauling goods",
_ => "going to eat",
};
if !at_home {
(p.x, p.y) = self.move_toward(pos, home);
}
}
("hunt", _, Some(r)) => {
p.action = "hunting";
let t = (self.rabbits[r].x, self.rabbits[r].y);
if cheb(pos, t) <= 1 {
if self.rng.chance(0.5) {
self.rabbits[r].dead = true;
p.food += 3;
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
}
(_, Some((kind, Some(t), label)), _) => {
p.action = label;
let idx = pos.1 * W + pos.0;
if pos == t && self.res[idx].0 == kind {
let take = yield_n.min(self.res[idx].1);
self.res[idx].1 -= take;
if self.res[idx].1 == 0 {
self.res[idx].0 = 0;
}
match kind {
BERRY => p.food += take as u32,
TREE => p.wood += take as u32,
_ => p.stone += take as u32,
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
}
_ => {
p.action = "wandering";
(p.x, p.y) = self.wander(p.x, p.y);
}
}
self.people[i] = p;
}
fn craft_and_build(&mut self) {
if self.wood >= 3 && self.stone >= 1 && self.tools < self.people.len() as u32 {
self.wood -= 3;
self.stone -= 1;
@ -436,103 +620,6 @@ impl World {
}
}
}
if p.hunger > 50.0 && p.food > 0 {
p.food -= 1;
p.hunger = (p.hunger - 25.0).max(0.0);
}
let home = self.home;
let yield_n = if self.tools >= self.people.len() as u32 {
2
} else {
1
};
let mut target: Option<(usize, usize)> = None;
let mut gather: Option<u8> = None;
let mut hunt = false;
if night {
if at_home {
p.action = "sleeping";
} else {
p.action = "going home";
target = Some(home);
}
} else if p.hunger > 50.0 && p.food == 0 && self.food > 0 {
p.action = "going to eat";
target = Some(home);
} else if p.carry() >= 6 {
p.action = "hauling goods";
target = Some(home);
} else {
let order: [u8; 3] = if self.food < 12 || p.hunger > 50.0 {
[BERRY, TREE, STONE]
} else if self.wood <= self.stone * 2 + 8 {
[TREE, STONE, BERRY]
} else {
[STONE, TREE, BERRY]
};
for k in order {
if let Some(t) = self.nearest_res(pos, k, 25) {
target = Some(t);
gather = Some(k);
p.action = match k {
BERRY => "foraging",
TREE => "chopping wood",
_ => "mining stone",
};
break;
}
}
if (self.food < 12 || p.hunger > 50.0)
&& (gather != Some(BERRY) || self.rng.chance(0.3))
{
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 25) {
target = Some((self.rabbits[r].x, self.rabbits[r].y));
hunt = true;
gather = None;
p.action = "hunting";
}
}
}
if let Some(t) = target {
if hunt {
if cheb(pos, t) <= 1 {
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 1) {
if self.rng.chance(0.5) {
self.rabbits[r].dead = true;
p.food += 3;
}
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if let Some(k) = gather {
let idx = pos.1 * W + pos.0;
if pos == t && self.res[idx].0 == k {
let take = (yield_n as u16).min(self.res[idx].1);
self.res[idx].1 -= take;
if self.res[idx].1 == 0 {
self.res[idx].0 = 0;
}
match k {
BERRY => p.food += take as u32,
TREE => p.wood += take as u32,
_ => p.stone += take as u32,
}
} else {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if !at_home || p.action == "going to eat" && cheb(pos, t) > 1 {
(p.x, p.y) = self.move_toward(pos, t);
}
} else if p.action != "sleeping" {
p.action = "wandering";
(p.x, p.y) = self.wander(p.x, p.y);
}
self.people[i] = p;
}
fn tick_rabbit(&mut self, i: usize) {
let mut a = self.rabbits[i].clone();
@ -541,14 +628,20 @@ impl World {
a.energy = (a.energy + 0.2).min(100.0);
}
let pos = (a.x, a.y);
if let Some(w) = Self::nearest_animal(&self.wolves, pos, 4) {
if self.rng.chance(0.8) {
let wolf = Self::nearest_animal(&self.wolves, pos, 4);
let c = Ctx {
wolf_near: wolf.is_some(),
roll: self.rng.f() as f64,
..Default::default()
};
match (self.decide("rabbit", c).as_str(), wolf) {
("flee", Some(w)) => {
let dx = (a.x as i32 - self.wolves[w].x as i32).signum();
let dy = (a.y as i32 - self.wolves[w].y as i32).signum();
(a.x, a.y) = self.move_dir(a.x, a.y, dx, dy);
}
} else if self.rng.chance(0.5) {
(a.x, a.y) = self.wander(a.x, a.y);
("wander", _) => (a.x, a.y) = self.wander(a.x, a.y),
_ => {}
}
if a.energy > 80.0 && self.rabbits.len() < 120 && self.rng.chance(0.01) {
a.energy -= 30.0;
@ -569,7 +662,13 @@ impl World {
let mut a = self.wolves[i].clone();
a.energy -= 0.15;
let pos = (a.x, a.y);
if let Some(r) = Self::nearest_animal(&self.rabbits, pos, 10) {
let prey = Self::nearest_animal(&self.rabbits, pos, 10);
let c = Ctx {
prey_near: prey.is_some(),
..Default::default()
};
match (self.decide("wolf", c).as_str(), prey) {
("hunt", Some(r)) => {
let t = (self.rabbits[r].x, self.rabbits[r].y);
if cheb(pos, t) <= 1 {
if self.rng.chance(0.4) {
@ -579,8 +678,8 @@ impl World {
} else {
(a.x, a.y) = self.move_toward(pos, t);
}
} else {
(a.x, a.y) = self.wander(a.x, a.y);
}
_ => (a.x, a.y) = self.wander(a.x, a.y),
}
if a.energy > 90.0 && self.wolves.len() < 10 && self.rng.chance(0.02) {
a.energy -= 40.0;
@ -597,6 +696,202 @@ impl World {
self.wolves[i] = a;
}
pub fn set_script(&mut self, src: &str) -> Result<(), String> {
self.vm = load_script(src)?;
Ok(())
}
fn rect(t: &[&str], i: usize) -> Option<(usize, usize, usize, usize)> {
let n: Vec<i64> = (i..i + 4).filter_map(|k| t.get(k)?.parse().ok()).collect();
if n.len() != 4 {
return None;
}
let c = |v: i64, max: usize| v.clamp(0, max as i64 - 1) as usize;
let (x0, x1) = (c(n[0].min(n[2]), W), c(n[0].max(n[2]), W));
let (y0, y1) = (c(n[1].min(n[3]), H), c(n[1].max(n[3]), H));
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();
let num = |i: usize| t.get(i).and_then(|s| s.parse::<i64>().ok());
match t.first().copied() {
Some("pause") => self.paused = num(1) != Some(0),
Some("speed") => {
if let Some(n) = num(1) {
self.speed = n.clamp(1, 8) as u32;
}
}
Some("stock") => {
if let (Some(name), Some(v)) = (t.get(1), num(2)) {
let v = v.clamp(0, 9999) as u32;
match *name {
"food" => self.food = v,
"wood" => self.wood = v,
"stone" => self.stone = v,
"tools" => self.tools = v,
_ => {}
}
}
}
Some("person") => {
if let (Some(id), Some(field), Some(v)) = (num(1), t.get(2), num(3)) {
if let Some(p) = self.people.iter_mut().find(|p| p.id as i64 == id) {
let u = v.clamp(0, 99) as u32;
match *field {
"hunger" => p.hunger = v.clamp(0, 99) as f32,
"food" => p.food = u,
"wood" => p.wood = u,
"stone" => p.stone = u,
_ => {}
}
}
}
}
Some("energy") => {
if let (Some((x0, y0, x1, y1)), Some(v)) = (Self::rect(&t, 1), num(5)) {
let v = v.clamp(1, 100) as f32;
let inside = |a: &Animal| a.x >= x0 && a.x <= x1 && a.y >= y0 && a.y <= y1;
for a in self.rabbits.iter_mut().chain(self.wolves.iter_mut()) {
if inside(a) {
a.energy = v;
}
}
}
}
Some("terrain") => {
if let (Some((x0, y0, x1, y1)), Some(kind)) = (Self::rect(&t, 1), num(5)) {
if (0..=6).contains(&kind) {
self.paint_terrain(x0, y0, x1, y1, kind as u8);
}
}
}
Some("res") => {
if let (Some((x0, y0, x1, y1)), Some(kind), Some(amt)) =
(Self::rect(&t, 1), num(5), num(6))
{
if (0..=3).contains(&kind) {
let amt = amt.clamp(1, 50) as u16;
for y in y0..=y1 {
for x in x0..=x1 {
let i = y * W + x;
let free = self.walkable(x as i32, y as i32)
&& (x, y) != self.home
&& !self.houses.contains(&(x, y));
if kind == 0 {
self.res[i] = (0, 0);
} else if free {
self.res[i] = (kind as u8, amt);
}
}
}
}
}
}
Some("spawn") => {
if let (Some(what), Some((x0, y0, x1, y1))) = (t.get(1), Self::rect(&t, 2)) {
let mut left = 25;
for y in y0..=y1 {
for x in x0..=x1 {
if left == 0 || !self.walkable(x as i32, y as i32) {
continue;
}
left -= 1;
let a = Animal {
x,
y,
energy: 70.0,
dead: false,
};
match *what {
"person" => {
self.spawn_person();
let p = self.people.last_mut().unwrap();
(p.x, p.y) = (x, y);
}
"rabbit" => self.rabbits.push(a),
"wolf" => self.wolves.push(a),
_ => left += 1,
}
}
}
}
}
Some("house") => {
if let Some((x0, y0, x1, y1)) = Self::rect(&t, 1) {
for y in y0..=y1 {
for x in x0..=x1 {
if self.houses.len() < 30
&& self.walkable(x as i32, y as i32)
&& self.res[y * W + x].0 == 0
&& (x, y) != self.home
&& !self.houses.contains(&(x, y))
{
self.houses.push((x, y));
}
}
}
}
}
Some("remove") => {
if let (Some((x0, y0, x1, y1)), Some(what)) = (Self::rect(&t, 1), t.get(5)) {
let inside = |x: usize, y: usize| x >= x0 && x <= x1 && y >= y0 && y <= y1;
let all = *what == "all";
if all || *what == "people" {
self.people.retain(|p| !inside(p.x, p.y));
}
if all || *what == "rabbits" {
self.rabbits.retain(|a| !inside(a.x, a.y));
}
if all || *what == "wolves" {
self.wolves.retain(|a| !inside(a.x, a.y));
}
if all || *what == "houses" {
self.houses.retain(|h| !inside(h.0, h.1));
}
if all || *what == "res" {
for y in y0..=y1 {
for x in x0..=x1 {
self.res[y * W + x] = (0, 0);
}
}
}
}
}
_ => {}
}
}
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;
}
self.tiles[y * W + x] = kind;
if !land {
self.res[y * W + x] = (0, 0);
self.houses.retain(|h| *h != (x, y));
}
}
}
if !land {
let ok = |tiles: &[u8], x: usize, y: usize| {
matches!(tiles[y * W + x], SAND | GRASS | FOREST | HILLS)
};
let tiles = self.tiles.clone();
self.rabbits.retain(|a| ok(&tiles, a.x, a.y));
self.wolves.retain(|a| ok(&tiles, a.x, a.y));
let home = self.home;
for p in self.people.iter_mut().filter(|p| !ok(&tiles, p.x, p.y)) {
(p.x, p.y) = home;
}
}
self.terrain_rev += 1;
}
pub fn terrain_json(&self) -> String {
let tiles: String = self.tiles.iter().map(|t| (b'0' + t) as char).collect();
format!("{{\"w\":{},\"h\":{},\"tiles\":\"{}\"}}", W, H, tiles)
@ -606,7 +901,10 @@ impl World {
let mut s = String::new();
let _ = write!(
s,
"{{\"tick\":{},\"day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
"{{\"paused\":{},\"speed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],",
self.paused,
self.speed,
self.terrain_rev,
self.tick,
self.tick / DAY + 1,
(self.tick % DAY) as f32 / DAY as f32 * 24.0,