Go to file
Steve Biedermann cdcbae4c05 feat(compose): run sqlite shards in separate containers
Co-Authored-By: GitHub Copilot <noreply@github.com>
2026-10-13 17:18:15 +02:00
docker docs: document the shard cluster, economy and per-shard databases 2026-10-12 20:43:11 +02:00
src feat(compose): run sqlite shards in separate containers 2026-10-13 17:18:15 +02:00
tests feat: run shards as separate processes behind a gateway 2026-10-12 20:29:11 +02:00
ui fix(access): restrict public clients to read-only 2026-10-13 16:46:00 +02:00
.dockerignore feat: add database persistence and Docker setup 2026-10-08 09:10:42 +02:00
.gitignore feat: run shards as separate processes behind a gateway 2026-10-12 20:29:11 +02:00
AGENTS.md dev: update AGENTS.md 2026-10-12 13:47:04 +02:00
CHANGELOG.md feat(compose): run sqlite shards in separate containers 2026-10-13 17:18:15 +02:00
CLAUDE.md rename AGENT.md to AGENTS.md 2026-10-12 10:41:49 +02:00
Cargo.lock feat(cluster): route entity messages by actor path 2026-10-13 16:33:23 +02:00
Cargo.toml feat(cluster): route entity messages by actor path 2026-10-13 16:33:23 +02:00
Dockerfile docs: document the shard cluster, economy and per-shard databases 2026-10-12 20:43:11 +02:00
Justfile feat(compose): run sqlite shards in separate containers 2026-10-13 17:18:15 +02:00
README.md feat(compose): run sqlite shards in separate containers 2026-10-13 17:18:15 +02:00
build.rs refactor(ui): resolve clippy pedantic lints 2026-10-11 21:23:50 +02:00
compose.yaml feat(compose): run sqlite shards in separate containers 2026-10-13 17:18:15 +02:00
config.rn feat: jobs, markets, haggling, carts and merchants 2026-10-12 20:10:22 +02:00

README.md

World Simulation

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.

Every person, rabbit, wolf, and partnership is a Picard actor that owns its state and applies its own rules: people age, get hungry, eat, and starve; animals spend and gain energy, breed, and die; partnerships track when they last had a child. The world keeps only world-level state (terrain, resources, the shared stockpile, houses, and the calendar) and talks to entities exclusively through messages: each step it asks the actors for their state, decides movement and opportunities with the Rune behavior scripts, and sends the actors the outcome. Behavior decisions stay on the simulation thread because Rune's VM is not Send, and replies are processed in entity order so runs stay deterministic. Picard's tracing and runtime statistics features are enabled: actor lifecycle events are logged (configure verbosity with RUST_LOG, for example RUST_LOG=picard=trace), and cumulative actor/message counters are shown in the UI.

Cluster

The world is split into vertical strips of columns called shards. Each shard is a separate process with its own Picard actor system and its own database. sim starts as a gateway: by default, it spawns SIM_SHARDS (default 3) copies of itself with the shard subcommand; when SIM_SHARD_ADDRS is set, it connects to externally managed shards instead. The gateway steps shards in lockstep and merges their views so the UI sees one world. Edits from the UI are routed to the shard that owns them.

People, animals and carts that cross a shard border are migrated with their whole state and inventory into the neighbouring shard's Picard actor system. A cluster-wide directory maps each entity address to its current shard and actor path, so world messages continue to reach it after migration without actor-owned forwarding timers or transfer messages. Shards connect directly through Picard's authenticated remoting endpoint (/remoting); the gateway continues to use /control for simulation coordination. The gateway generates a shared token when it launches shard processes; external shard deployments supply the same SIM_CLUSTER_TOKEN to the gateway and every shard.

Public access

Clients outside localhost can view the simulation but cannot edit it: the UI hides editing controls, and the server rejects remote write requests and WebSocket edit commands. When exposing the app through a reverse proxy, keep the published port bound to host loopback and ensure the proxy sets X-Forwarded-For or X-Real-IP. Compose enables SIM_TRUST_PROXY_HEADERS=1 for this setup. Docker-host requests to localhost remain editable, while forwarded requests are classified by their client IP.

Economy

Every shard founds markets and workplaces: farms, woodcutters, quarries, hunters, toolmakers and cartwrights. Adults are hired, commute to their job site each day, work their shift, sell the goods at a market, get paid and go home. Farmers sow fields that ripen into crops; gatherers fell trees, quarry stone and hunt; crafters buy materials and make tools and carts. Markets are actors with their own coins and stock; prices follow supply and demand, and buyers and sellers haggle. Merchants carry surplus goods to markets on other shards. Markets, workplaces, carts and crop fields are drawn on the map, and the side panel shows each person's job and coins.

Actor Console

The server enables Picard's console by default and exposes its JSON request/response protocol over ws://127.0.0.1:7878/api/actors/ws. The connection reaches shard 0; use ?shard=N to connect to shard N. Every shard registers the others as console peers named shard-N: peers lists them, @shard-1 actors forwards a command, and send @shard-1 PATH JSON sends a message to a remote actor through Picard remoting. Send one JSON request per text frame; each receives one response. For example, {"id":1,"op":"actors"} lists actors, and {"id":2,"op":"inspect","path":"/user/person-1"} inspects one (actors are named person-<id>, rabbit-<id>, wolf-<id>, and partnership-<id>). Other operations include messages, pause, resume, clear, stop, restart, and send. Console requests are handled by the selected shard's Picard console. cargo run --no-default-features disables the endpoint; --features console enables it explicitly.

The simulation script is the root config.rn, a single Rune file. Its pub const items hold defaults and tuning values: map dimensions and terrain generation, initial population and resources, calendar and life-stage defaults, hunger, gathering, construction, partnerships and births, animal behavior, and edit limits. Its pub fn person, pub fn rabbit, and pub fn wolf items are the agent behaviours, and they can use the constants directly. Every constant the server reads must be defined; missing or invalid values are reported at startup. SIM_CONFIG can point to a different Rune file. Docker Compose mounts the repository's config.rn read-only into the gateway and each shard container.

The in-browser script editor edits the same file contents. Applied edits take effect immediately for behaviours and tuning values, while map dimensions, initial population, and calendar defaults apply when the world is reset. Editor changes are not written back to disk: on restart, config.rn from disk replaces them, even when a saved world snapshot is restored.

The Calendar and life-stage durations panel configures ticks per day and the number of days, weeks, and months in larger calendar units. Each human life stage has an inclusive minimum/maximum duration in in-game days; the world samples within that range when a person enters the stage. These runtime settings are stored with the world snapshot and take precedence over the config defaults until the world is reset.

New people, including babies, start as toddlers and age through child, teen, young adult, adult, older adult, elderly, and death. By default, the 192×128 world starts with 180 people (60 of them young adults), 1200 rabbits, 80 wolves and 1200 food, shared between the shards. People pathfind around blocked terrain when travelling home for food. Unpartnered people from young adulthood onward can form a single partnership with a nearby eligible person. Each partnership can have one toddler per calendar week when both partners are in stages marked fertile, the stockpile has enough food, and housing capacity permits it. By default, the village has capacity for 12 people, with each additional house increasing capacity by three; these values are configurable. Young adult and adult stages are fertile by default; fertility can be changed per stage in the calendar panel. Partnerships, markets, workplaces and carts are Picard actors as well as people and animals.

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

Local run with SQLite

Install Rust with rustup. The build script installs the wasm32-unknown-unknown target if it is missing. Start locally with SQLite using:

just dev

Open http://127.0.0.1:7878. Each shard keeps a SQLite database sim-shard-N.db in the current directory, and the initial seed is 42. You can also run directly with cargo run.

To choose a seed on the first run:

cargo run -- 123

Existing shard snapshots take precedence on later runs, including over a new command-line seed. If the shards' snapshots don't match (for example after changing SIM_SHARDS), all shards start fresh. Use the UI's Reset button to start a fresh world with its current seed, or Regenerate to choose a different seed.

Run with Just

Install Just, then list the project recipes:

just

Useful recipes:

just run  # Stop Compose services, then build and start the gateway and SQLite shards
just stop # Stop Compose services; keep shard data volumes
just dev  # Run locally with SQLite

Run with Docker Compose and SQLite shards

Install Docker with the Compose plugin, then run just run. This stops any existing Compose stack, removes obsolete service containers, then starts one gateway container and three shard containers. Each shard stores its snapshot in its own SQLite database and persistent volume. You can also use the equivalent Docker command directly:

docker compose up --build -d --remove-orphans

Open http://localhost:7878. The shard_0_data, shard_1_data, and shard_2_data volumes survive container restarts, just stop, and docker compose down.

Compose now uses SQLite instead of Postgres. Existing Postgres snapshots are not migrated; the old Postgres volume is left untouched, and the SQLite shards start fresh.

To remove all shard volumes and saved simulation data as well:

docker compose down -v

Database Configuration

Each shard selects its database through SHARD_DATABASE_URL. {shard} in the URL is replaced with the shard index and is required when there is more than one shard:

Backend Example URL
SQLite (default) sqlite://sim-shard-{shard}.db?mode=rwc
Postgres postgres://USER:PASSWORD@localhost:5432/sim_shard_{shard}

For example, run locally against a reachable Postgres server that has the shard databases with:

SHARD_DATABASE_URL='postgres://USER:PASSWORD@localhost:5432/sim_shard_{shard}' cargo run

Every shard saves its part of the world as a snapshot about once per second and restores it at startup. Compose uses a separate SQLite database and volume for each shard; Postgres remains available for deployments that configure it explicitly.

SIM_SHARDS sets the number of shards (default 3). SIM_SHARD_ADDRS optionally provides comma-separated shard control addresses in index order; set SIM_CLUSTER_TOKEN to the same cluster token on the gateway and every external shard. SIM_ADDR sets the gateway's listen address. It defaults to 127.0.0.1:7878; containers set it to 0.0.0.0:7878.

Project Layout

  • src/world.rs contains the simulation and snapshot serialization; src/world/ holds the actors, shard routing and economy.
  • src/main.rs runs the gateway's Axum server and HTTP/WebSocket API, and the shard subcommand.
  • src/cluster/ contains the gateway, the shard node, the inter-shard transport and the control protocol.
  • src/storage.rs provides SQLite and Postgres snapshot storage.
  • config.rn is the Rune simulation script: pub const defaults and tuning values plus pub fn behaviours.
  • ui/ contains the Leptos frontend, compiled to Wasm by build.rs.
  • Dockerfile, compose.yaml, and Justfile provide container and developer workflows.

Tests

Run the server tests with:

cargo test --workspace --no-default-features

tests/cluster.rs starts a real multi-process cluster.