Skip to content

Repository files navigation

smallworld

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.

The village

Run it

npm install
npm run dev          # server on :8787, client on :5173

Open 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:8b

No GPU, or you just want to see the machinery move?

npm run smoke        # three in-world days, headless, no model needed

The model

.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.

What a villager can do

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.

What is here

Looking at a villager

  • 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.

What is deliberately not here

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.

How it is put together

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.

Prompt size is the throughput budget

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

Configuration

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.

Adding to the world

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.

Licence

MIT.

About

A village of six people where every choice is made by a language model. Ten verbs, no behaviour trees.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages