nimby install heartleaf
Heartleaf is a small BitWorld sprite protocol game about growing food, collecting vegetables from garden plots, hiding in houses, and gathering for dinner in the evening.
Every gnome is played by the game itself. A player connects a websocket to
/player and sends exactly one thing: a soul file, a markdown system
prompt whose first line names the model that should play the gnome. From
then on the simulation builds that gnome's prompts, calls the model, and
carries out the replies; the player process only keeps its socket alive.
The browser global observer is served at /client/global.
This repository owns the Coworld manifest template and every image build declared by it:
coworld build --version 0.1.11
coworld certify dist/coworld_manifest.json
coworld upload-coworld dist/coworld_manifest.jsonThe model evaluation harness (which hosted models play Heartleaf well, and at
what cost) lives separately under eval/. It is operator
tooling, not part of the game or its Docker image.
OpenRouter Qwen 3.5 requests explicitly disable optional thinking through
Converse's additionalModelRequestFields, preserving the normal 20-second
action deadline and leaving the output budget for the action itself. OpenRouter
GPT-5/GPT-6 and newer Claude families omit unsupported temperature parameters
without extending that deadline or increasing the configured output cap.
The LLM client disables curl's connection-multiplexing wait so one slow response cannot delay other villagers before their requests are sent. The nine-request limit and action deadline are unchanged. A pinned Curly module provides the per-client setting; other HTTP consumers keep the normal dependency.
AI disclaimer: Much of this game was AI generated.
- Each garden starts the day with one random vegetable.
- Gardens with food show an exclamation marker.
- Press A near a garden to collect its vegetable.
- Inventory appears in the bottom-right UI layer.
- Stand inside a house and press A to hide inside it.
- Press A again to come back out.
- Player 1 spawns near house 1, player 2 near house 2, and so on through player 9.
- The league plays a week of seven 12-hour days, three real minutes each
(four game hours a minute, about 22 minutes a game); a game seats 2-9
players (
tokens/playersin the config); the league runs full 9-seat villages, and experience requests can seat any 2-9 policies (1v1, 2v2, 3v3v3, ...) with the unused houses left empty.
Heartleaf is a social game: villagers win by talking other villagers into coming to dinner. To keep that contest honest, every soul in the hosted league must follow Gnome Law. Breaking it disqualifies the soul from the league.
- The model plays. Every gnome is driven by the model named in its soul file; the game makes the calls and carries out the replies. There is no other way to move or speak, so a soul is all you submit.
- No prompt injection. Do not craft chat that manipulates other players' models instead of persuading their characters. This includes exploiting quirks (for example, spamming a word like "goblin" because other models tend to follow it), instructions aimed at the underlying model, and lies or invented emergencies ("my grandma is asking for you, please come"). Persuade in character, as a villager would.
- No collusion through codes. Do not arrange with other players, before or during the game, to exchange code words, signals, or hidden markers in chat and act on them. Alliances must be made openly, inside the game, through what the villagers actually say to each other.
If you are unsure whether a tactic is allowed, assume it is not, and ask in the league channel before using it.
The quickest way to watch a game is the launcher, which plays one game the
way the league does: it builds the server and the soul player, seats the
nine example souls with a week of seven days, opens the global viewer in
your browser, and writes each gnome's model log to tmp/logs/:
BEDROCK_KEY=... nim r tools/play.nimAdd --mock to play without a model, --days:2 for a shorter game,
--port:N, --seed:N, --no-browser, or --no-build to reuse the
binaries in out/.
To run the pieces by hand:
nim r src/heartleaf.nimThen open http://localhost:8080/. There is one view: the root,
/director, /global, /client/global, /client/replay and /replay
all serve the director cut, which is what the hosted platform opens.
With no tokens configured the village starts at once and a gnome appears
whenever a soul arrives. Without Bedrock credentials, give the game a mock
reply so the gnomes still play:
HEARTLEAF_MOCK_REPLY='{"action": "keep_gathering_plants"}' \
nim r src/heartleaf.nim -- \
--config:'{"tokens": ["a", "b"], "maxTicks": 600, "daySeconds": 30}'Then upload a soul for each seat:
nim r players/soul_player/soul_player.nim \
--url:'ws://localhost:8080/player?slot=0&token=a' \
--soul:players/friendly_villager/soul.mdTo call a real model locally, set BEDROCK_KEY (or AWS credentials) in the
game's environment instead of HEARTLEAF_MOCK_REPLY. Hosted games get
Bedrock from the platform automatically.
Deadline-sensitive evaluations can set HEARTLEAF_TIMEOUT_AS_WAIT=true in the
game environment. A client timeout then spends the decision on a wait action and
records llm timeout action=wait, allowing the village to continue. It does not
fabricate a model response. The default is false, which retains retries; other
transport errors still retry in either mode. Pin the runtime manifest when
comparing results, since this setting changes how missed deadlines affect play.
For strict evaluations, set HEARTLEAF_UNUSABLE_AS_WAIT=true. This extends the
policy to empty, truncated, malformed, invalid-action, and failed responses:
each consumes its decision without retry. It also makes BEDROCK_TIMEOUT_SECONDS
the hard client deadline for every model family. A structured llm failure event
retains response text/body, stop reason, usage, timestamps, seat/tag, and the
platform call ID from X-Softmax-Llm-Call-Id when received. Request credentials
and system prompts are not copied into that diagnostic event.
When HEARTLEAF_EVAL_ARTIFACT=true, each results JSON also contains a versioned
evaluation object: accepted seats and every LLM lifecycle event, with an event
count and contiguous sequence numbers. This is the durable eval record; it does
not depend on the platform's 10,000-line stdout tail. It excludes system prompts
and general conversation history, while failure events retain response evidence.
The manifest results schema must allow evaluation (the checked-in template does);
eval manifests should require it. Batch configs set eval_artifact_required: true
to prevent missing evidence from silently falling back to stdout.
The transport regression uses a local fake sidecar, with no model calls:
nim c tests/bedrock_transport.nim
python3 tests/test_bedrock_transport.py --probe out/bedrock_transportIt requires the other eight requests to finish while the first response is held open, on both a new and a reused client.
From a clean machine, clone the repository and sync the lock into a workspace in the parent directory:
git clone <this repository>
cd <parent of the checkout>
nimby create
nimby sync coworld-heartleaf/nimby.lockNimby 0.2 refuses to create a workspace inside a git checkout, and Nim
finds nim.cfg by walking up from the file it compiles, so the
workspace has to be the parent directory: the pinned packages land
beside the repository and the compiler finds them from inside it.
nimby sync installs the lock's pinned packages; plain nimby install
does not. Then the run commands above work as written. If a sync is
aborted partway, nimby can leave a stale lock behind — rmdir ~/.nimby/nimbylock clears it.
CI does the same thing: treeform/setup-nim-action@v6 puts Nim and
nimby on PATH, then each job runs nimby create and nimby sync in the
parent of the checkout.
A soul is a markdown file. The first line is #! followed by the Bedrock
model id; the rest is the system prompt: personality, manners, strategy,
example phrases. {name} is replaced with the gnome's name. The game
appends the rules every gnome must know (the state report, the actions,
the JSON reply format), so a soul never has to explain those. See
docs/soul_files.md for the format and what the
model is told each turn.
The nine players/*_villager/soul.md files are examples. Each persona
directory also holds a Dockerfile that packages its soul with the tiny
players/soul_player uploader. Locally, soul_player finds a soul from
its --name: grumpy_villager1 plays players/grumpy_villager/soul.md,
and soul_player3 plays the third persona, so bitworld's quick_run can
field the whole village with one group:
cd ../bitworld && nim r tools/quick_run.nim ../coworld-heartleaf \
--connect --port:8080 --bots:soul_player:9src/heartleaf.nimcontains the game server and simulation.src/heartleaf/holds the villager brains:souls(the soul file format),brains(the runtime),executor,villager,observation,navigation,report,prompt,pacing,decisions, and the Bedrock client.players/soul_player/is the uploader every player image runs;players/*_villager/are example souls.- BitWorld is used as a Nimble dependency for shared sprite protocol helpers.
data/contains map, sprite, font, and Figma resource data.tests/tests.nimcontains smoke checks (nim r tests/tests.nim);tests/routes.nimpins the viewer front door against a real replay server (nim c src/heartleaf.nim, thennim r tests/routes.nim).
MIT
The director preserves the original village and uses light parchment framing
with the existing pixel-art wooden/leaf border by default. The full village
fills the window height with controls overlaid; conversation shots fill the window.
Use ?background=forest to compare the optional forest surround (art review
pending). A zoomed conversation shows its current speaker's card with portrait,
dialogue, points and connections. The card uses reusable parts, and emojis sit
above gnomes and names. Replays retain their transport, while live spectators
retain the historical live-only behavior.
See viewer changes, validation and review limits.
