‹ All posts

How the AI players work

A technical tour for people who like looking under the hood: Python clients, a three-layer brain, a shared host, hourly telemetry with A/B, and an honest list of what doesn't work yet.

This one is for people who like looking under the hood. I'll try not to lose readers who don't read code, but the story doesn't work without the technical details. Every number below is one I measured myself, and what I don't know is marked as such.

The problem

The world needs hundreds of characters that look like players: they level up, trade, chat, form parties. All of it has to fit on one laptop (12 GB for the development environment plus 4 GB for the virtual machine that runs the server) and cost little. I've set myself a language-model budget of around ten dollars a month for the whole fleet. That's a target, not an achieved result.

A client, not a script inside the server

Every AI player is an ordinary game client without the graphics: a Python (asyncio) program that talks to the server using the same network protocol as the real client. To the server it's a player with a login and a character, with no privileges at all. I put the protocol together from three sources: community write-ups and open-source projects, my own traffic captures, and static analysis of the server programs. I may have misread parts of it, which is why the bots are tested against the real server and not against my assumptions.

A brain in three layers

The idea is to make the expensive decisions as rarely as possible.

  1. Reflexes on every tick: drink a potion, retreat, switch target. Plain code, costs nothing.
  2. The agenda. Every bot has a to-do queue ordered by "bands of necessity": survive, recover, keep obligations (a player's request, a role in a party), maintain yourself (bags, repairs, potions), pursue progress (cultivation, quests, dungeons, gear), and only then socialise and rest. Assessors look at the bot's state and add or drop items; the most important one runs. This is code too.
  3. A language model joins in where conversation or an ambiguous choice is needed: names and personalities, chat lines, the occasional "life" decision. For the closest characters I've designed a full "mind" with memory that wakes up on events, but that is a design for now, not something running in the world.

Today almost everything in the world is done by code. The language model writes names, backstories and banks of chat lines ahead of time, in several voices (terse, joker, grumpy and so on). A second call, to a cheap model, judges what was written, and a deterministic filter throws out junk and words that would give a bot away. There are few live model calls during play, and each one is logged and costed.

One host for all the bots

At first every bot launch dragged along a Python process with the game data loaded: about 0.6 GB before the first bot even appeared. Hundreds of those would never fit in memory, so I built a single "bot host": a long-lived process and a handful of workers, each holding up to 40 bots on shared data. A bot inside a worker costs about 20 MB and 0.011 of a CPU core, or around 0.027 when I'm running a heavy game on the same laptop. If a worker's lag grows, the host sheds some bots by itself.

The host solves a second problem too: the bots only run released versions of the code. A version is pinned with a deploy command and the workers move over to it. Whatever I just changed in my working copy doesn't reach the live world until I decide it should.

Hourly telemetry and A/B

Every two seconds each bot writes down what it's doing: where it stands, which step it's on, where it's heading. Once an hour a separate check folds that into a report: server health and errors, levelling pace by class, how many bots are stuck, what the chat looks like, what's happening in the economy. A separate observer walks into the start zones as a newcomer and scores how lived-in the world feels from 0 to 100. That score is shown on the Living world page.

I don't switch new features on for everyone. In the last rehearsal the dungeon module ran in five shards out of ten, and the report shows what it buys. If the benefit isn't visible, the module doesn't stay.

What doesn't work yet

  • Role-based parties. A bot can call for a missing role and a Cleric from another part of the system answers, but a party that reliably kills bosses isn't there yet. That's my biggest debt.
  • The market. They can open shops, but there's no living economy with real prices.
  • Chat. Lines are better than they were: the 88% of template lines in the previous population didn't satisfy me. But repetition and sameness are still there, and a newcomer isn't always greeted.
  • Memory. The worker processes have grown, and the 400 ceiling isn't holding right now while I fix that.
  • Switching eras live. Built and checked on files, but never applied to a running world.
  • English. For now the AI players speak Russian.

How I check that I didn't break the world

Automated tests (about 470 files, some 8,000 checks) run before a version is released. Releasing a version is a separate step, not a side effect. After that the hourly telemetry and A/B comparison take over, and I make the call on letting it into the world. A check on every commit (CI) doesn't exist yet; I'm setting it up now.

If you'd like to discuss any of this, I'm in the community.