I've been experimenting with Claude Code to write my own agent for automatically playing MUDs. It was really the 90s, when I last spent a lot of time with MUDs, but they're kind of a perfect testing ground for autonomous agents. Two things had popped on my radar recently as inspiration.

A million years ago, back in 2024, I first saw this YouTube video by "Two Minute Papers" that got stuck in my head. It looks like an SNES game and shows a little town full of LLM-powered NPCs, all working together to plan a party. Something about that concept has been rattling around upstairs ever since. Then last week, in a BlueSky post, someone suggested building an agent that plays MUDs while simultaneously taking notes and journaling from its own point of view. And now.. I can't seem find the post anymore.. but thank you, stranger who inspired me, I will link you if I can figure it out.

SO YEAH, ANYWAY.. I decided to try building an agent.

I wanted to run the MUD locally, and after a quick consultation with ChatGPT, I decided to start with CircleMUD. The codebase is available and well-documented. Claude Code was able to get it running without any problems.

The immediate task was to build the main loop. Getting something that "worked" was easier than expected. There are basically 3 parts:

  1. World <-> Agent communication (telnet)
  2. Agent <-> LLM communication (http)
  3. Agent internals (glue, game state, system prompt, etc)

I built the telnet and http parts completely separately. Claude knows how to "speak telnet" by writing a python script. I ran a local LLM server using llama.cpp and a variant of Facebook's Llama-8B-Instruct model.

Before reaching Step 3, Claude Code had already figured out my plan to glue them together. I didn't need to explain anything about building an agentic loop or parsing JSON into commands. It knew exactly what I was trying to do.

It works. I wouldn't say it works WELL, yet. The agent can issue commands like "look table" and "north" to move around the world. It can interact with mobs and NPCs. Sorta. The commands are pretty random and the agent's only goal is to "explore". It does that, but without intention. It feels promising, but incomplete. I think I can make it a better.

Claude Code is great at fuzzing

Maybe it's not fuzzing precisely, but more like "protocol discovery". For example, when the telnet client connects, it immediately encounters a menu system for player login or new character creation, which looks like this:

By what name do you wish to be known? <input>
Did I get that right (Y/N)? <input>
New character. <input>
Give me a password: <input>
Please retype password: <input>
What is your sex (M/F)? <input>

Select a class:
  [C]leric
  [T]hief
  [W]arrior
  [M]agic-user

Class: <input>

PRESS RETURN:  <input>

Welcome to CircleMUD!
0) Exit from CircleMUD.
1) Enter the game.
..

Make your choice:  <input>

Before starting as a new character, the player needs to make NINE inputs in order to actually join the game. Claude picked this up immediately. It explored most of the branches and extended the client with logic to navigate these menus. Quite cool and clever.

In fact, this reminds me a lot of MPC. The MUD itself is conceptually very similar to an MPC server. It has its own internal representation of the world, and its own internal APIs. The agent interacts by discovering and interacting with that protocol through plain text.

Function Calling (Tool Use)

I took the approach that in-game commands are essentially functions with parameters. I'm not sure if that was the best option in this specific situation, but it worked just fine.

The way this works is:

  1. The prompt includes a list of available tools (commands) and preferably, examples of how to use them
  2. The LLM emits JSON roughly in the format {"command": "look", "parameters": ["north"]}
  3. The agent parses the JSON, converts it to the syntax of the MUD, and sends that command over telnet

I'm curious whether treating this formally as "function-calling" was actually helpful here. The MUD command interpretor is somewhat flexible with inputs. Would just plain text have been better since all the commands are pretty simple? Dunno, something to circle back on. This worked for now.

Command Adherence

The first little problem was command adherence. Sometimes the LLM would choose an invalid command, or invalid parameters, or a command that could not be used at that specific moment.

To correct this, I added a series of warnings to the prompt. If the LLM issues an invalid command, the next prompt would include this text:

[IMPORTANT: Your last command was invalid. Please review the help information below and try a different approach.]

And the warnings escalated if additional invalid commands were attempted. After 4 unrecongized commands, the prompt would say:

[ALERT: Invalid command detected. Use the "help" command to get a list of available commands.]

This was meant to serve two purposes. First, the prompts provide alternatives, like suggesting the "help" command. Second, they provide "jitter", to provoke some additional randomness and possibly nudge the LLM into responding differently. I think this approach worked pretty well, but it's something I'd like to experiment with more.

Avoiding Repeated Commands

The next problem was repeated commands. The initial agent lacked any internal memory/history. Often times, the same command would be attempted over and over again. The first extended interaction looked something like this (yes, the ATM is not a mistake or hallucination, its an actual object in the first room of this fantasy-themed game!)

Step 1:  look
Step 2:  examine automatic teller machine
Step 3:  examine automatic teller machine
Step 4:  examine automatic teller machine
Step 5:  examine automatic teller machine
Step 6:  help automatic teller machine
Step 7:  examine automatic teller machine
Step 8:  get money
Step 9:  look
Step 10: help

The agent is immediately very interested in using the ATM, and it never tries anything else. I added a small history buffer to the prompt, so the LLM could remember what it had already tried.

It's important to remember that LLMs have no memory (or at least, my LLM doesn't). Every piece of information available to the LLM must come from its offline training, or be included in the context window. I modified the agent to retain a list of every command, and to include the most recent commands in the system prompt. Approximately like this:

You are a text-based adventure agent...
<other stuff>

These were your last 4 commands:
- look north
- north
- take sword
- take sword

That sorta worked. I quickly extended this to also include the abbreviated outcome of each command. Issuing repeated commands is fine in some cases (ex: traveling in the same direction to explore the map), but its a dead-end in other situations (trying to open the locked treasure chest, when you don't have the key). I think that including the world-response to each command helped distinguish these cases.

Next Steps

I was impressed by the degree to which making a MUD-playing agent "just worked". This was largely helped because Claude Code had understood very quickly what I was trying to do. Being able to zip through the character creation menus was huge as well. That would have been a major slog for me to do from scratch.

There are a bunch of things that I'm interested in exploring more here. Just a few of them:

  • modifying the CircleMUD codebase to be more agent-friendly
  • multiple agentic characters acting together as a party
  • using game logs to automatically self-improve
  • building a world map from the agent's perspective
  • planning and goal setting
  • trying different LLMs

I expect to continue experimenting with CircleMUD for awhile. But now I'm wondering whether its crazy to build my own MUD engine in order to have better control over the game world.