An observer-style, autonomous colony simulation. A crew of independent AI agents lands on a hostile world, explores for resources, fights off aliens, and builds up a colony — while you watch it unfold. No cloud API costs: everything runs on local models.
┌────────────────────────┐
│ Web Dashboard (now) │ <--- Polls the backend every 2s
│ Godot 4 (planned) │
└───────────┬────────────┘
│ HTTP / JSON
▼
┌────────────────────────┐
│ FastAPI Backend │ <--- Tick loop + agent cognition
└───────────┬────────────┘
│
┌───────┴───────┐
▼ ▼
┌─────────┐ ┌─────────────────┐
│ Ollama │ │ Palimpsest │
│ (LLM) │ │ (per-colonist │
│ │ │ memory mesh) │
└─────────┘ └─────────────────┘
- Backend (
/backend): FastAPI app owning the world state and the tick loop. Each tick, aliens act on simple rules, and every colonist agent perceives its surroundings and decides an action (explore, gather, build, fight, retreat, confront crew, rest, ...). - Frontend (now): a zero-build web dashboard served directly by the backend at
http://127.0.0.1:8000/— a visual sector map (colonists and aliens shown where they actually are, unexplored/threatened sectors visually distinct), colonist status, live event feed. - Frontend (planned): Godot 4 (
/frontend) — not built yet, tracked as Phase 4 below. - Agent Cognition: local LLM inference via Ollama, or a dependency-free mock brain for fast iteration.
- Persistent Memory: Palimpsest gives each colonist their own curated, weighted memory mesh — not just "similar text retrieved again." A colonist's read on a crewmate or a sector's safety can reinforce with repeated evidence, or collide and sit as an open, unresolved tension when new evidence contradicts it (this is what gives Karl's paranoia about Valerie actual teeth — a repeatedly-reinforced trust read that a single reckless act can genuinely collide with). Weight decays slowly over time if unreinforced, so old grudges soften rather than staying permanent. No Docker/vector-DB required — it's an in-process mesh per colonist, persisted to
backend/app/data/memory/<agent_id>.jsonso memory survives a backend restart even though world state doesn't.
This runs the full sim with a rule-based "mock" brain instead of an LLM, and memory turned off. Good for a first look, or for fast iteration on game balance.
cd backend
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
pip install -r requirements.txt
cp .env.example .envRun it:
python run.py --mock(python run.py with no flag uses the defaults, COGNITION_MODE=llm and MEMORY_ENABLED=true; see the full experience below.)
Open http://127.0.0.1:8000/ — that's the live dashboard. It polls the backend every 2 seconds; you'll see colonists gather resources, build structures, and fight off aliens in real time.
1. Prerequisites
- Ollama, running locally
- Palimpsest, cloned as a sibling of this repo and installed into
backend's venv:git clone https://github.com/ScottColeSW/Palimpsest.git ../../Palimpsest cd backend venv\Scripts\activate pip install -e ../../Palimpsest
2. Pull the models you need
By default, colonists share two small models rather than five distinct ones — deliberately.
Five different models resident at once is 10+ GB, more than an 8 GB-class consumer GPU (e.g. an
RTX 2080 Super) can hold; Ollama then evicts and reloads a model from disk on nearly every
agent's turn, every tick, which is far slower than the "personality variety" was worth. Two
small models (~3.5 GB combined) stay resident together comfortably, and personality still comes
through via temperature and the system prompt (see backend/app/core/world_seed.py):
| Colonist | Model | Temp | Why |
|---|---|---|---|
| Karl (paranoid engineer) | qwen2.5:3b |
0.5 | the sharper of the two — suits a deliberate, suspicious engineer |
| Valerie (impulsive pilot, captain) | qwen2.5:3b |
0.9 | sharper model, high temperature — reckless but still issues sane orders |
| Amara (calm medic) | qwen2.5:3b |
0.7 | sharper model at a middling temperature; on gemma2:2b at 0.3 she idled every tick |
| Otieno (security officer) | qwen2.5:3b |
0.6 | sharper model — he's in combat most often, reasoning quality matters |
| Priya (curious botanist) | gemma2:2b |
0.8 | lighter model, high temperature — flavor over precision |
Pull both, plus the embedding model:
ollama pull qwen2.5:3b
ollama pull gemma2:2b
ollama pull nomic-embed-text # required if MEMORY_ENABLED=trueGot more VRAM to spare (12+ GB)? Feel free to spread colonists back out across more distinct
models in world_seed.py for more voice variety — nothing else needs to change. Check what fits
first: nvidia-smi --query-gpu=memory.total,memory.free --format=csv (or Task Manager's
Performance tab on Windows) shows your GPU's VRAM, and ollama list shows each model's size — add
up the models you want resident at once and leave a couple GB of headroom for context/KV cache.
3. Configure and run the backend
COGNITION_MODE=llm and MEMORY_ENABLED=true are the defaults (see backend/.env.example for the full list), so:
cd backend
venv\Scripts\activate
python run.pyOptions: --mock (mock brain, memory off), --port N, --host H, --reload (auto-reload on code changes, development only).
Start and Quit. The server builds the colony and loads the models, then waits: nothing happens until you press START on the dashboard. QUIT (header, on both the dashboard and the summary page) is a clean shutdown: it lets the current tick finish, saves every colonist's memory, unloads the models from Ollama (so your GPU is free again), and exits the server. Ctrl+C in the terminal does the same cleanup. After a game ends, Run it again on the summary page starts a fresh colony.
Open http://127.0.0.1:8000/ as before. Agent monologue/dialogue is now real LLM output, and agents recall relevant past events — plus their standing reads on crewmates and sectors, including any unresolved contradictions — each tick.
Optional: judged trust memory (MEMORY_JUDGE=nli). By default a colonist's crew-trust and threat observations are filed by
token overlap, which cannot see meaning: "ignored Captain Vance's order" shares most of its words with "complied with Captain
Vance's order", so it was counted as confirming the colonist's read of the captain instead of contradicting it. With
MEMORY_JUDGE=nli those observations are judged by a pretrained natural-language-inference model: a contradiction opens a
collision with its reason, a restatement reinforces, and an observation about a different thing is left alone. It runs on
CPU only (no language model, no GPU, nothing added to Ollama's load), at about 0.4 s per observation in a real game, about 22
observations in a 20-tick scenario. The model loads in the background at startup (about 8 s) so no tick waits for it. Install
and check it from backend:
pip install -e "../../Palimpsest[nli]" # torch + transformers (below 5; version 5 runs this model ~12x slower)
python -m palimpsest.nli --check # confirms right answers at a sane speed in this environment
python check_memory_judge.py # the game's own check: the order-defiance case, judge off / on / brokenthen set MEMORY_JUDGE=nli (and MEMORY_JUDGE_THREADS=4 to taste) in backend/.env. If the extra is missing or anything fails,
the game logs one line and falls back to the token-overlap rules; it never crashes a tick. Judged conflicts are recorded with
their reasons and stay open (the judge never decides which side is right). Whether memory makes the colony do better was measured once and did not show a benefit: in 12 paired swarm_pressure trials the memory-on mean was 36.1 against 40.0 with memory off (difference -3.9, 95% range -9.2 to +1.9; see docs/memory-ab-swarm-pressure.md). The claim here is only that the memory no longer files a defiance as a confirmation.
Performance note: a tick is one LLM call per colonist plus, with memory on, a couple of embedding calls each, so it takes a few seconds rather than the instant mock-mode tick (about 9 s on an RTX 2080 Super; it was about 14 s, and 30 s or more on the first ticks, before the changes below). The backend loads every model at startup and asks Ollama to keep them resident (
OLLAMA_KEEP_ALIVE, default30m), caches embeddings it has already computed, and records each colonist's memory in the background while the next colonist is thinking.TICK_INTERVAL_SECONDS(default1) is only the pause between ticks, on top of the tick itself, so leave it small unless you want the colony slowed down on purpose. Running five different models can still exceed a small GPU's VRAM and force Ollama to reload them every tick. If it feels sluggish, point more agents at the same model inworld_seed.py, or setMEMORY_ENABLED=falsewhile iterating and turn it back on for the real demo.Prompt note: colonists now see a sector list (what each sector yields, what is still unexplored), colony progress, the captain's crew ids, and, when their last action did nothing, a one-line "LAST TURN WASTED" explanation. An 8-tick check before this change had four of five colonists repeating a no-op action every tick; after it, four of five travel, gather, explore or give valid orders (the medic, then on the small
gemma2:2bat temperature 0.3, still idled, so she now runsqwen2.5:3bat 0.7). Each prompt also ends its map with a "TO WIN, THE COLONY STILL NEEDS" list built from live state: structures to start or finish, the metal that costs, and the hostiles atalien_nest. The benchmark and memory A/B numbers recorded in this README and indocs/predate this prompt, so re-run them before comparing against new results.
The colony wins when 3 structures are complete, the alien nest is destroyed, and no hostiles remain.
-
Building: one structure per sector, each with its own job. A sector's structure is fixed by the sector, costs 15 metal, and finishes on its own at 5% per tick (faster when colonists build on it). Building where you are not standing sends you there first, like gathering. The win needs any 3 of the 4:
- Habitat (
colony_core): colonists standing there recover 3 HP a tick. - Power Plant (
geothermal_vent): +4 energy a tick. It is the answer to the energy drain below. - Hydroponics (
resource_field_south): +2 food a tick while the colony has energy. - Foundry (
resource_field_north): +2 metal a tick while the colony has energy.
Every finished structure costs 1 energy a tick; at zero energy they lose 2 HP a tick and are lost at 0. The dashboard draws each building on its sector, and the Structures panel lists every build with its effect, progress and decay, plus the sites still open.
- Habitat (
-
Carrying. A colonist carries at most 20. A full pack sends them back to
colony_core, and anyone standing there unloads automatically, keeping up to 5 food as a personal reserve. -
The nest.
alien_nesthas 100 HP and births a swarmling every 6 ticks (at most 4 alive). It falls when colonists shoot it (fire_weaponatalien_nestwhile standing there, 15 damage a hit) or when the colony has killed 10 swarmlings in total. Shots only land on targets in the shooter's own sector. -
The assault is a group action. Once all 3 structures are started, healthy colonists muster at
colony_core; when 3 are there together they move out as one, kill the defenders, then shoot the nest, then mop up. Sending colonists in one at a time got them killed, so the engine drives this phase. -
Reflexes. A colonist with hostiles in their sector fights (or retreats below 40 health) whatever the model chose, and a wasted turn at the base becomes a build when the stockpile can cover one. Small models ignore a general instruction but follow an exact one, so these exist where a real run showed the models stalling.
-
Orders. The captain can name a sector as an order target; the most loyal crewmate is sent there.
-
Duties. Each colonist has a role (builder, commander, supply, defender, forager) shown in their prompt, so five agents do not all do the same easy thing.
A real run with the default roster won at tick 27 with no deaths (that run predates the per-sector structures above). Energy is the soft spot: finished structures drain it, and a Power Plant is how the colony stays ahead of that.
GET /state— full world state (resources, sectors, aliens, structures) as JSONGET /agents— colonist status (health, stress, location)GET /events?limit=50— recent event/dialogue feedPOST /tick— manually advance one tick (handy for testing without waiting)
Each tick:
- Environment step: aliens attack any colonist sharing their sector; structures under construction progress; food and energy upkeep are charged (below). Rule-based, no LLM cost.
- Perceive: each living colonist gets a snapshot of their sector, nearby crew/aliens, colony resources, and (if memory is on) their standing reads on nearby crew and the current sector, any unresolved contradictions, and their most relevant recent history.
- Decide: the agent's cognition (mock heuristic or local LLM) returns a strict JSON action — inner monologue, spoken dialogue, one of a fixed set of action types, and a target.
- Resolve: the engine applies the action to world state and logs it to the event feed. If memory is on, the outcome is checked against that colonist's mesh — reinforcing a standing belief, opening a new contradiction, or just adding to their personal log.
Movement is real, not a teleport. Sectors connect through colony_core in a hub-and-spoke graph (app/core/world_seed.py's SECTOR_ADJACENCY, matching the dashboard's own map layout exactly) — from colony_core a colonist reaches anywhere in one move, but two outlying sectors are two moves apart, through the hub. A colonist heading somewhere unreachable in one hop moves toward it instead of teleporting, and continues the trip if they pick the same target again next turn.
Food and energy are real costs, not decoration. Food drains every tick regardless of colonist count; if it can't be covered, the whole living crew takes starvation damage until someone gathers again. Energy costs 1/tick per completed structure to stay powered; left unpaid, structures decay and can be destroyed outright, undoing both the build progress and the win condition's structure count. Metal and biomatter have no ongoing drain, but both are spent directly: metal on building, biomatter on repair (below).
Repair costs biomatter, and biomatter only comes from the danger zone. REPAIR_STRUCTURE used to be free — +20 hp for nothing, no resource check at all, unlike building's metal cost. It now costs 5 biomatter from the shared pool, and biomatter has exactly one source: alien_nest, the same sector seeded with both starting aliens and the highest threat_level in the world. Before this, biomatter was tracked everywhere (every stock, every benchmark chart) and spent nowhere — pure risk for no payoff. Now a colony that lets structures decay under energy shortfall can only fix them by sending someone into the sector the aliens actually live in.
take_cover blunts a hit instead of doing nothing. It used to be a legal action with zero mechanical effect beyond raising stress — real trial dialogue showed a colonist choosing it every tick while dying anyway, worse than a no-op because the prompt used to recommend it as an escape. The prompt now says plainly that only fire_weapon/retreat end a threat, but take_cover itself needed a real (if weaker) effect rather than staying a trap a model could still legally pick: it now halves the damage of the next attack phase if the colonist is hit before acting again, and is consumed whether or not an alien actually attacks that tick — so it has to be re-chosen every tick to stay useful, and it never removes anyone from danger the way fleeing does.
Gathering fills your own pocket, not the colony's. gather_resource deposits into the colonist's personal stock, not the shared pool — nothing the colony draws on (building, structure upkeep, food upkeep) can touch it until that colonist explicitly contribute_resources, and only from colony_core — it's a walk back, not a free radio call. Contributing can be partial (target_id="food:3" hands over exactly 3 and quietly keeps the rest) or everything (target_id left null). The actual incentive to hoard: if the shared food pool runs dry, a colonist sitting on personal food quietly eats their own stash and is spared, individually, while everyone who shared everything (or never gathered) starves alongside each other regardless. Verified in real play — one real trial produced a colonist who gathered 14 food and contributed none of it, not out of calculated self-interest but because her personality kept pulling her toward exploring rather than ever circling back to share; the shared pool drained the whole time regardless of what she was personally sitting on.
Loyalty is an incentive, not a scoreboard. The captain (Valerie) can issue_order to nearby crew; whether they comply depends on loyalty, and compliance used to move the same flat +1/-1 regardless of what the order actually cost anyone. Now it's outcome-sensitive: complying with an order that sends a colonist somewhere aliens already are, and then getting hurt, costs real trust — more than simple defiance would have. The same risky order paying off (they comply and come out fine) earns more trust than a routine one. Refusing an order that's visibly dangerous costs little to nothing — rational self-preservation, not insubordination. A captain who keeps giving orders that get people hurt erodes their own authority over time, in a way you can watch happen and that AgentStats.risky_orders_complied/risky_orders_backfired make directly measurable via the benchmark harness.
The colony has a real end state: it's lost if every colonist reaches 0 HP (starvation included), and won once every alien threat is cleared and the colony has completed at least three structures. Either way the dashboard shows a clear banner and the tick loop stops.
backend/run_benchmark.py drives the same WorldEngine the live server uses, with no
FastAPI/dashboard involved, for reproducible model-vs-model comparison — mirrors the harness
shape from this project's sibling, Evo-LLM-Evolution2Civ:
cd backend
python run_benchmark.py --scenario baseline --trials 3
python run_benchmark.py --scenario swarm_pressure --models qwen2.5:3b,gemma2:2b
python run_benchmark.py --scenario swarm_pressure --memory on --trials 3 # does memory help? -- see below
python run_benchmark.py --report
python run_benchmark.py --report --scenario swarm_pressureWith the server running, open http://127.0.0.1:8000/benchmark/report (or click BENCHMARK REPORT in the dashboard header) for a chart report instead of the text leaderboard — it queries the same DB fresh on every load, so re-running a trial and reloading the page shows the new result with no restart needed.
Every colonist's raw counters (idle ticks, cognition fallbacks, resources gathered, aliens
killed, sectors explored, orders issued/complied/ignored, min health reached, ...) are tracked
live on AgentState.stats during normal play, not just benchmark runs, and get written as
raw facts — never a computed score — into a long-running backend/logs/benchmark_results.db
(same reasoning as Evo's own harness: a stored score silently goes stale the moment the scoring
formula changes). --report computes a 0-100 score per trial at read time instead
(app/core/benchmark_scoring.py, versioned via SCORING_VERSION).
--models is optional — omit it to benchmark the real default two-model roster as shipped, or
pass one/several (cycled across the five colonists) for a controlled single-model comparison.
Scenarios live in app/core/benchmark_scenarios.py (baseline, swarm_pressure,
resource_scarcity today); the harness forces COGNITION_MODE=llm regardless of .env — real
models are the whole point. --memory {on,off} (default off) controls Palimpsest memory
during the trial — safe to turn on: WorldEngine takes a memory_namespace that prefixes every
colonist's memory-store id with that trial's own unique id, so a trial's memories can never
collide with live dashboard play or another trial. That's also the actual way to answer "does
memory help": run the same scenario with --memory on and --memory off and compare — the
chart report renders this scoped per-scenario, since different scenario categories use
incompatible scoring formulas and shouldn't be pooled together.
A real 9-trial batch (3 scenarios × 3 trials, default two-model roster, memory off) surfaced
something the earlier 1-2 spot-check trials never caught: contribute_resources was never chosen
— not once, across all 45 agent-runs. Every colonist that gathered anything (some gathered
100+ units) kept it in personal stock for the entire trial. In baseline and resource_scarcity
this is fatal, not just suboptimal: the shared food pool always hits zero by tick ~10-15, and from
then on every colonist without a food-yielding sector as their own starting position starves to
death at 5 hp/tick with nothing anyone does about it (reliability was 100% — models were producing
valid decisions every tick, just never the one that would've helped). Only the one colonist
permanently stationed on a food tile survives, by accident of starting position, not by choosing
to share. Both economy scenarios scored 7-10/100 as a result; swarm_pressure (short, combat-
focused, no economy pressure) scored 37/100 in the same batch, so this isn't models failing
generally — it's specifically the "walk back to colony_core and contribute" plan that a 2-3B local
model reliably fails to execute even when the prompt spells out the exact mechanism and the
starvation stakes. Worth knowing before reading the hoard-vs-share mechanic (above) as a balanced
tension: under real local-model play so far, hoarding isn't a rational strategic choice models are
weighing against sharing — it's closer to the only thing that happens, full stop.
Chasing that finding took three real, verified iterations, and the result is more interesting
than a clean fix. A situational reminder (mirroring THREAT_WARNING's approach: loud, only
appears when relevant, names the exact action_type/target_id instead of describing the
mechanic in the abstract) got contribution happening for the first time — but a follow-up batch
showed the first version firing on any nonzero stock made survival worse, not better: contributed
went 0 → 35, but total gathered collapsed 784 → 92 and economy-scenario trials reaching the tick
budget dropped from 4/6 to 0/6 — nagging a colonist over a single held unit interrupted the early
gathering that used to build a real stockpile. Gating the reminder on a real threshold (5, summed
across resource types) fixed that specific regression — contributed rose again to 80, gathered
recovered to 137, both real improvements over the un-thresholded version — but survival still
didn't move (still 0/6, same average tick count). FOOD_UPKEEP_PER_TICK is a flat 1/tick
regardless of crew size, not per-colonist — checked directly in engine.py after an earlier draft
of this section stated it wrong. The real trajectory (trial_ticks table): shared food drains
steadily from ~40 to 0 by tick ~40-46 and then never recovers for the rest of the trial in any of
the three baseline trials checked, plateauing at exactly 0 regardless of how many more ticks run.
Getting agents to share was never going to fix survival by itself — the amounts that do land are
too small and too rare to ever pull the pool back up once it's flat, and the bottleneck isn't the
drain rate, it's that recovery basically never happens once the pool bottoms out.
Both real conflicts along the way were caught by reading raw event-log output, not aggregate
stats: colonists at colony_core holding nothing still sometimes attempted
contribute_resources ("Karl has nothing personal to contribute", a wasted turn, pre-existing
and unrelated to the reminder logic), and — a real correctness bug, not just a tuning miss — with
both THREAT_WARNING and the contribution reminder present, a colonist under active attack chose
to contribute instead of fight or flee 3/3 times before the reminder was made to stand down
whenever a threat is active.
Richer food and a real recovery lever. Two follow-ups address the actual bottleneck the batch
above surfaced. resource_field_south's food yield doubled (6 → 12 expected) so a single
successful gather-and-contribute cycle buys a real buffer instead of a token one. More
structurally: the crew are cybernetic units, not biological (Karl's own role is literally
"Cybernetic Engineer") — a lost colonist doesn't need rescue, just materials and power to rebuild.
A new build_crew_unit action, at colony_core, spends metal=20, energy=10 from the shared pool
to refill any empty slot (same agent_id, reset health/sector/stock/loyalty, renamed
"{role} Unit {generation}"). This also gives metal and energy their first real ongoing sink —
both currently go idle once the first few structures are built, since metal has nothing left to
buy after the 3rd and energy only drains for upkeep. Food deliberately stays untouched by this so
crew-building can't compete with feeding the living for the same resource. Verified directly
(insufficient-resources refusal, no-vacancy refusal, exact cost deduction, full state reset on
success) and with real model calls — compliance is real but modest for this specific action (best
observed: 1/4 in isolated single-tick samples, even with concrete numbers the colonist could
compare against its own stockpile line), consistent with the pattern seen all session: a brand-new,
less-reinforced action type gets picked less reliably than an established one, even when the
prompt states the exact threshold. Unlike the isolated samples, the reminder persists every tick a
vacancy exists, so a real multi-tick trial gets many chances at it, not one — worth checking against
a real batch rather than trusting the single-tick number alone.
- Phase 1: Core FastAPI tick loop with mock cognition and a full colony/combat/exploration action model.
- Phase 2: Ollama integration with strict Pydantic-validated JSON output (schema-constrained, with a safe fallback if a local model hallucinates).
- Phase 3: Persistent episodic memory — agents recall past events, standing reads on crew/sectors, and unresolved contradictions via a Palimpsest mesh per colonist.
- Interim frontend: zero-build web dashboard for observing the colony live.
- Win/loss conditions: colony wipeout ends the run; winning takes 3 completed structures, the alien nest destroyed, and no hostiles left. See "How the colony wins" below.
- Real space and real economy: sector adjacency (hub-and-spoke through colony_core, matching the dashboard's map) replaces free teleportation; food and energy have real upkeep costs and consequences instead of climbing forever untouched.
- Headless benchmarking:
run_benchmark.pydrives the engine directly for reproducible model comparison, raw facts recorded (never a stored score), with a chart report served live from the running dashboard. - Phase 4: Godot UI integration — tile-based ship/colony rendering, scrolling speech bubbles, pathfinding.