Getting Your Hands Dirty
Building a MUD
I’m building a MUD. A Multi-User Dungeon. In 2025. In TypeScript.
If you’re under thirty, you probably just said “a what?” If you’re over thirty-five and spent any time on the internet before it had pictures, you might have felt something stir in your chest just now. That’s nostalgia. Or maybe heartburn. Either way, let me explain.
What MUDs Were (and Why They Mattered)
Before World of Warcraft, before EverQuest, before Ultima Online, before graphics cards were even a given, there were MUDs. Text-based multiplayer games running on university servers, accessed over Telnet, played by nerds who thought 14.4 kbps was screaming fast.
You’d connect to a host and port, and you’d get something like:
The Town Square [n, s, e, w]
You are standing in a cobblestone square. A fountain burbles
in the center. To the north, the road continues toward the
castle gates. A shady-looking merchant leans against the
southern wall.
> look merchant
The merchant eyes you with suspicion. He is wearing a
threadbare cloak and appears to be concealing something
beneath it.
> say What are you selling?
You say, "What are you selling?"
The merchant whispers, "Things you can't afford, friend."
That was the entire interface. No sprites, no UI framework, no asset pipeline. Just text in, text out, and your imagination filling in the gaps. And somehow it was more immersive than most games I’ve played since, because your brain is a better renderer than any GPU ever built. It just is.
MUDs were also the birthplace of concepts we now take for granted in online games. Guilds, raids, PvP zones, crafting systems, experience points, player housing: MUDs had all of it first. Richard Bartle’s player taxonomy (Achievers, Explorers, Socializers, Killers) came from studying MUD players. The man literally co-wrote MUD1, the first one, in 1978.
I grew up on this stuff. BBSes first, then MUDs when I could get internet access. I spent hundreds of hours in worlds made entirely of words, and I’ve wanted to build one ever since. So now I am!
Why TypeScript, Why Now
The original MUDs were written in C, and later in LPC (a language literally created for building MUDs; the LP stood for Lars Pensjö, its creator). Some were in Python, some in Java. There’s a long tradition of MUD codebases being passed around, forked, modified, and re-released, like folk songs with semicolons. I love that.
I chose TypeScript for a few reasons, none of them trendy.
First, I know it. I write it every day, I think in its type system, and when I’m prototyping at 2 AM I don’t want to be fighting with memory management or null pointer exceptions. I want to describe my game world in types and let the compiler catch the dumb stuff. That’s the whole pitch.
Second, Node.js gives me something the original MUD developers would have killed for: native async I/O. A MUD server is fundamentally an I/O-bound application; it’s shuffling text between dozens or hundreds of connected clients, processing commands, updating game state, and pushing descriptions back out. Node’s event loop is built for this. No thread pools, no mutex locks, no deadlocks at 3 AM because two players tried to pick up the same sword.
Third, and this is the real reason: TypeScript lets me run the same language on the server and in the browser. Which matters because EllyMUD doesn’t just speak Telnet.
Three Protocols, One Game World
Here’s where it gets fun. Or at least where it gets fun for me, which is the same thing when you’re the one writing it. 😄
EllyMUD runs three servers simultaneously:
Telnet on port 8023. The classic. RFC 854, the ancient protocol I grew up on. Full sub-negotiation support: NAWS (Negotiate About Window Size), TTYPE for terminal detection, MCCP (MUD Client Compression Protocol) for bandwidth. When your terminal suddenly switches to line mode in the middle of a fight, you learn what IAC WILL LINEMODE looks like real fast.
WebSocket on port 8080. Same game world, browser-based clients. Connect from a React app, an iPad, wherever. The game engine doesn’t know or care how you got there.
REST API on the same port. This is where it gets interesting.
The trick is a ConnectionManager that abstracts the transport layer. Telnet, WebSocket, or a virtual in-memory connection for testing: they all produce the same Connection interface. The game engine just sees players. Clean.
The Admin Panel (Yes, In The Browser)
This is the part where my ops instincts took over and I accidentally built a control plane for a text game. No regrets.
EllyMUD ships with a full React admin dashboard that connects over the API. Here’s what you get:
Overview Panel: Server stats, uptime, connected player count, tick rate. The stuff you’d expect from any monitoring dashboard, except the thing being monitored is a fantasy world with goblins.
Players Panel: Every connected player, their health, mana, level, current room, idle time, connection type (Telnet vs WebSocket). You can kick players, send admin messages, and, here’s the one I’m proudest of: live monitor any player’s session in real-time. You see exactly what they see, every command, every output. It’s like screen-sharing but for a text game.
Config Panel: Live-edit game configuration without restarting the server. Tick intervals, save frequency, spawn rates. Hot-reload everything.
World Builder: This one’s wild. A drag-and-drop visual room editor built with React Flow. You create rooms, draw exits between them, edit descriptions, all in a node graph. The visual representation of your dungeon layout, rendered as a flowchart. When you’re building a 50-room dungeon, this is the difference between “fun creative work” and “I want to quit and become a farmer.”
You can edit room descriptions, item properties, NPC configurations: all from the browser, all persisted to whichever storage backend you’re running. No SSH required. No text editor. Just open the dashboard and start building your world.
157 Test Files and a Very Weird E2E Strategy
Okay, I need to brag about this because nobody tests a hobby MUD like this and I know it’s a little unhinged. Here’s the breakdown:
59 unit test files covering every single command. look.test.ts, attack.test.ts, buy.test.ts, sell.test.ts, equip.test.ts, even snake.test.ts (yes, there’s a playable Snake game inside the MUD; yes, it has tests). One test file per command. I have standards.
6 end-to-end test files using a testing method I’m genuinely proud of.
4 integration test files for storage backends, data migration, and Redis sessions.
The E2E tests are the interesting part. I built a TesterAgent class that spins up a full GameServer in test mode and plays the game like a real player. It connects, logs in, sends commands, and asserts on the cleaned output. Here’s what makes it special:
const agent = await TesterAgent.create();
const session = await agent.directLogin('testplayer');
const output = await agent.sendCommand(session, 'look');
expect(output).toContain('Town Square');
That’s a real test. The agent handles all the protocol noise: strips ANSI codes, removes HP/MP prompts, cleans cursor control sequences. What you get back is just the game text, ready for assertions.
But it goes further. The agent can freeze time with advanceTicks(), so combat is deterministic. It can snapshot and restore game state for clean test isolation. It can manipulate player stats directly. And it supports multiple simultaneous sessions, so you can test two players interacting in the same room.
The combat E2E test suite runs a full encounter from attack goblin through damage ticks to NPC death and loot drop, all in under a second, with deterministic timing. That’s wild for a networked multiplayer game.
And because the TesterAgent also works in remote mode (just set MCP_URL and it connects to a running server instead of spinning up its own), the same tests work against a live instance. CI tests hit the embedded server; I can point the same suite at production to smoke-test a deploy.
The MCP API: Where LLMs Actually Cook
This is where the text-based nature of MUDs becomes a superpower that graphical games will never have.
EllyMUD exposes 30+ MCP tools over JSON-RPC. Model Context Protocol, the standard for giving AI agents structured access to external systems. An LLM connects to EllyMUD and gets tools to create sessions, send commands, query game state, read logs, manipulate player stats, and control the game timer.
Here’s what that unlocks:
AI-driven development. The entire game is text-based. Input is text, output is text. That means an LLM can play the game, notice something is missing, and implement it. I’ve done this: pointed Claude at EllyMUD via MCP, said “play the game for a while,” and when it tried a command that didn’t exist, I said “here’s the session log, implement the missing command.” It read the log, saw the failed command, looked at how existing commands are structured, wrote the implementation and the unit test, and the command worked on the next session. That’s not a hypothetical workflow. I do this regularly.
Testing on autopilot. Remember the TesterAgent? It works through MCP too. AI agents can wander the game world trying things, and because every action is text, the LLM naturally understands what happened. “I tried to drop an item while in combat and the server crashed” is a perfectly legible bug report from an AI tester. 🛌 At 3 AM. While I was asleep.
NPCs that actually talk. Instead of branching dialogue trees (soul-crushing to write, feels like a flowchart to play), hook up an LLM as an NPC. The merchant in the town square doesn’t need a dialogue tree. He needs a personality description, a list of things he sells, and an instruction: “you are a suspicious merchant who doesn’t trust strangers.” The conversations write themselves, differently every time. That’s kind of magical!
Emergent gameplay. Multiple AI agents, each with different goals and personalities, inhabiting the same game world as human players. The goblin warband isn’t scripted; it’s agents following survival instincts. That’s the kind of emergent behavior that made the original MUDs magical, except now the NPCs can actually think.
The text-based format is the whole trick. A graphical game would need computer vision for an AI to understand what’s happening. A MUD just needs to read. LLMs were literally built for this.
54 Commands and Counting
The game currently has 54 implemented commands, each with its own file and test suite:
Movement, combat, spellcasting (magic missile! mana management! cooldowns!), a 12-slot equipment system (head, neck, chest, back, arms, hands, finger, waist, legs, feet, main hand, off hand), merchants with buy/sell, banking with deposit/withdraw, equipment repair, NPC combat with spawn systems and aggression levels, status effects (poison, stun, root, buffs), rest and meditation for health/mana regen, training for stat upgrades, and a fully playable Snake game you can play inside the MUD. Because why not.
Pluggable Storage
EllyMUD supports four storage modes: flat JSON files (edit with a text editor, zero friction), SQLite (single file, no deps, the default), PostgreSQL (for when this inexplicably takes off), and auto mode (database with JSON fallback). Swapping backends is a config flag, not a rewrite. The server auto-migrates data between backends on startup.
Why Text Games Ship
I’ve tried to build graphical games before. Everyone has. You spend six months on an asset pipeline and a character controller and you still don’t have a game, you have a tech demo with a capsule sliding around a grey plane. 😩
Text games skip all of that. The “rendering engine” is process.stdout.write(). The “asset pipeline” is a markdown file with room descriptions. The game design starts on day one, because there’s nothing between you and the gameplay except your ideas and a TCP socket.
That’s why I’m building a text game nobody asked for. Because someone asked for every graphical game, and none of them shipped. This one already has 54 commands, 157 test files, a visual world editor, AI-driven NPCs, and a goblin that keeps trying to befriend the players instead of fighting them.
I still haven’t decided if that’s a bug or a feature. 🤷♀️