A pixel-art city simulation built in Godot 4, where autonomous agents are driven by a local LLM. Each agent chooses its destination — and reacts to events like fire. Developed as part of a research internship at the Machine Learning Group, UiT The Arctic University of Norway (Tromsø), intended for display at the Vitensenter (science center).
- Prerequisites
- Quick Start
- Using the Game
- LLM Server Setup (vLLM + Docker)
- Alternative: Ollama
- Configuration
- Project Structure
- Runtime Logs
- Known Limitations
- Godot 4.x
- Docker with the NVIDIA Container Toolkit configured (for the primary vLLM setup)
- An NVIDIA GPU (developed and tested on a Jetson AGX Orin 64GB)
- Start the local LLM server — see LLM Server Setup below.
- Open this project folder in Godot (
project.godot). - Check that
llm_base_urlinscripts/agents/agent.gdpoints to your running LLM server (default:http://localhost:8001). - Open and run the main scene (
scenes/DefaultMap.tscn).
- Camera / view: the game runs in portrait orientation, designed for the Vitensenter's vertical displays.
- Agents: each agent (villager, lumberjack, firefighter, houseman, traveller, bird, etc.) picks its own destination autonomously via the LLM. You can click and drag any agent to reposition it manually — it will resume its own decision-making once released.
- Fire tool: use the 🔥 icon in the toolbar to click anywhere on the map and ignite a fire. Fires spread automatically over time and are fought by scripted firefighter agents. Non-firefighter, non-bird agents can be killed by fire and will respawn at their assigned home, having "learned" a lesson from the experience (see Known Limitations).
- Day/night cycle: runs automatically (2-minute cycle); agents with an assigned home are nudged to return there at night.
- Survival: agents have hunger/thirst gauges that drain over time and are scripted to refill at a designated well/refill point — this does not involve the LLM.
⚠️ Jetson-specific. The setup below uses a Docker image built for the Jetson AGX Orin's ARM64 architecture (nvidia-ai-iot/vllm:latest-jetson-orin) and requires the NVIDIA Container Toolkit. On a generic x86 PC without a dedicated NVIDIA GPU, skip ahead to Alternative: Ollama.
This is the primary setup used for development. The game needs a local LLM server running continuously in the background, exposing an OpenAI-compatible API with log-probability scoring support.
chmod +x setup_vllm.sh
./setup_vllm.shThis script:
- creates a
vllm-qwencontainer servingQwen/Qwen2.5-3B-Instruct-AWQon port8001 - sets
--restart unless-stoppedso it restarts automatically on every boot (as long as Docker itself starts on login — see the script's output for how to check this) - is idempotent — safe to re-run if the container already exists
Verify the server is running and responding:
docker logs -f vllm-qwen # follow model loading
curl http://localhost:8001/v1/models # should return the model once readyFor a machine without a dedicated NVIDIA GPU (e.g. a deployment machine without the Jetson):
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:3b-instruct
sudo systemctl edit ollamaAdd inside the editor:
[Service]
Environment="OLLAMA_HOST=0.0.0.0:8001"sudo systemctl daemon-reload
sudo systemctl restart ollama
curl http://localhost:8001/v1/modelsThen update llm_model / CRITIC_MODEL in agent.gd to "qwen2.5:3b-instruct" (the Ollama model name, different from the HuggingFace name used with vLLM).
Note: Ollama's support for
echo/logprobson/v1/completions(used for the destination-scoring mechanism) has not been as thoroughly validated as vLLM's. After switching, checkuser://transition_log.jsonlto confirm probabilities vary meaningfully between candidates rather than being uniform or empty.
scripts/agents/role_config.gd— role definitions (villager, lumberjack, firefighter, houseman, traveller, bird, etc.)scripts/agents/agent.gd— LLM connection settings (llm_base_url,llm_model), movement speed, individual memory, fire-handling logicscripts/agents/agent_memory.gd— persistent per-agent memory (lessons learned after dying to fire)
res://
├── scenes/ # Godot scenes (map, UI)
│ ├── agents/
│ ├── buildings/
│ ├── nature/
│ ├── ui/
│ └── roads/
│ ├── DefaultMap.tscn # Main map
├── scripts/
│ ├── agents/ # agent.gd, role_config.gd, llm_scorer.gd, agent_memory.gd
│ ├── environment/ # world_registry.gd, road_graph.gd, zone_context.gd, fire_manager.gd
│ ├── ui/ # toolbar, menus
│ └── core/ # fire.gd, water_jet.gd
└── assets/
The game writes logs during execution outside the project folder, in Godot's user data directory (user://):
| File | Contents |
|---|---|
transition_log.jsonl |
Every scored destination decision (transition matrix) |
token_log.jsonl |
Per-decision token/logprob breakdown |
fire_decision_log.jsonl |
Fire-avoidance decisions (continue/avoid) |
agent_memory/<agent>.json |
Persistent per-agent memory (Reflexion-style lessons) |
agent_memory/reflection_log.jsonl |
Full history of post-mortem reflections |
Location: in Godot, Project → Open User Data Folder.
These files are not version-controlled (see .gitignore) — they are machine- and session-specific.
- Agents show a selection bias tied to the first token of a place's name rather than its geographic position or role semantics — see the research logs for details of the experiments run on this.
- The fire-avoidance mechanism depends on a quantized 3B model; learned lessons are not guaranteed to be followed consistently — check
fire_decision_log.jsonlto measure the actual avoidance rate over time.
