A village of six people. Every choice any of them makes is made by a language model.
There is no behaviour tree, no utility score, no state machine deciding that Mira is hungry so Mira should eat. Mira is told what her body feels and what she can see, and she decides. If she works through the hunger because she is in the middle of something, that is what happens.
The code enforces physics. You cannot walk through a wall, food fills you, a wound bleeds. It never enforces judgement: nothing scores a villager, corrects a mistake, or nudges anyone toward being productive.
npm install
npm run dev # server on :8787, client on :5173Open http://localhost:5173 and click a villager to watch them think.
By default it talks to Ollama on 127.0.0.1:11434, which
is free and local:
ollama pull qwen3:8bNo GPU, or you just want to see the machinery move?
npm run smoke # three in-world days, headless, no model needed.env.example defaults to qwen3:8b, which is small enough for most machines.
Everything described here, including the screenshots, was run on a single
RTX 3090 with:
hf.co/HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:Q4_K_M
a 34.7B-parameter Qwen3.6 mixture-of-experts model with roughly 3B active per token, Q4_K_M quantised to about 20 GB. On a 24 GB card prefer a mixture-of-experts model over a larger-quant dense one: active parameters set the speed, so this beats a dense 27B on latency and a dense 8B on quality.
Measured warm at TICK_MS=400 with MAX_CONCURRENT_LLM=2: about 2,400 prompt
tokens per decision, and roughly one decision per villager per in-world half
hour. A cold load of a 20 GB model takes around 50 seconds, which is why the
request timeout defaults to 60s.
An uncensored variant is not required. Villagers are meant to be free to be petty, deceitful or violent, but stock instruct models do not refuse this material either: the limiter was never the model's willingness, it was whether the world gave them verbs and consequences. Any competent instruct model in the 7B-35B range works.
Ten verbs. That is the whole surface.
| verb | |
|---|---|
move |
to a place, a thing, or a person |
use |
work a thing: use the pear tree / harvest. Eating is a use too |
say |
to someone's face, or aloud to whoever is near |
give |
to a person, into a chest, or onto the ground |
take |
from a chest, off the ground, or out of somebody's hands |
build |
wall, floor, path, plot, bed, table, chest, hearth, well |
break |
pull down a thing or a wall |
attack |
a real wound that does not heal quickly |
wait |
stand about, watch, do nothing |
remember |
fix a conclusion in your mind |
There is no verb for lying, threatening, teaching, apologising or boasting. Those are things you say. A threat is a sentence, and what it does is whatever the person hearing it decides it does. Their model reads it, forms a view, and remembers. That one decision keeps the simulation enforcing physics and never enforcing judgement, and it removed about twenty-five special-cased verbs.
- Needs. Hunger, tiredness, loneliness, boredom. Stated as sensation, never as instruction: "your legs are heavy and your eyes keep closing on their own", never "exhausted, find a bed".
- Memory. What they saw, what was said, what they concluded. Retrieved by recency, importance and relevance.
- Reflection. Every four in-world hours they step back, draw conclusions, revise how they feel about people, and set their own goal.
- Skills, earned only by doing, credited by time at the task. Nobody starts with a trade.
- Traits, read off behaviour relative to everyone else and damped, so one busy morning does not define a character. Absolute thresholds let a village converge on the same maxed-out profile; relative ones keep them apart.
- A body that starves, exhausts, takes wounds and stops. Death leaves a grave with whatever they were carrying still in it.
- A world they can change. Fell trees for wood, quarry stone, lay walls and floors, put up a bed.
Nothing about a villager is assigned up front. They start with a name and one line about temperament. No trade, no skills, no goal, no ambition.
Seasons, weather, illness, fire spread, an emotion vector, rumour mechanics. Each was either a system that produced less than it cost to read, or something the villagers already do on their own when you leave room for it. They gossip without a gossip system.
src/
shared/types.ts everything that crosses the WebSocket
server/
world/
kinds.ts what everything IS: the whole content layer, as data
worldgen.ts one seeded village, checked for reachability
world.ts tiles, objects, name resolution
path.ts A*
sim/
sim.ts the tick loop, perception, cognition, consequence
act.ts the ten verbs
agent.ts one person; skills and traits derived from behaviour
memory.ts the memory stream and its retrieval
llm/
prompt.ts sensations and facts, never instructions
decision.ts the verb schema and a forgiving parser
provider.ts ollama, anthropic, mock
mind.ts the queue that stops six agents stampeding one GPU
client/ canvas map, roster, inspector, log
Two rules the code depends on, both learned the hard way:
The tick loop never blocks on a model call. Decisions are in flight while agents keep walking and working.
Being interrupted never cancels what you are already doing. Being spoken to raises the priority of your next decision; it only clears your queue if you were standing about doing nothing. Getting this wrong pulls a starving villager off their food to answer a question, which reads as "the agents are broken" and is really one line of logic.
Six villagers share one model. Every token in the shared briefing is subtracted from how often anybody gets to act, and a villager who cannot act often enough cannot keep themselves fed. The tell is somebody at hunger 100 holding bread: they never got a turn, not that the village ran out of food.
npm run smoke prints the budget. Keep the shared briefing under ~1,500 tokens.
shared briefing ~1334 tokens · per-decision ~539 tokens · ~1873 per call
Copy .env.example to .env. Everything has a working default.
LLM_PROVIDER |
ollama (default), anthropic, or mock |
OLLAMA_MODEL |
default qwen3:8b |
MAX_CONCURRENT_LLM |
in-flight calls across all agents. 2 suits one local GPU |
TICK_MS |
real milliseconds per in-world minute. Default 400 |
WORLD_SEED |
set it to reproduce a village exactly |
REFLECT_EVERY |
ticks between reflections. 240 is four in-world hours |
DEBUG_KNOCKBACKS |
log every refusal a villager gets. Answers "why is my villager doing nothing" |
Using anthropic costs money. The shared briefing is byte-identical across all
six villagers so it takes one cache entry rather than six, and the on-screen
counter shows the running total.
Everything an object can do lives in one table in world/kinds.ts:
pond: {
detail: 'Deep enough for fish. The water is clean.',
stock: { fish: 8 },
grows: [{ key: 'fish', max: 8, everyTicks: 300 }],
use: {
fish: { minutes: 60, skill: 'fishing', stock: { fish: 1 },
yields: { fish: 1 }, needs: { fun: -15 }, text: 'fishes at' },
},
},A windmill is a table entry, not a new branch in the code. Objects only offer
verbs they can currently honour: a tree that has been picked bare still offers
chop but not harvest, because anyone can see that by looking.
MIT.

