Table of Contents
I found a C# dungeon RPG I wrote in 2014, got it running on .NET 8, and then rebuilt it from scratch as a sci-fi browser game in TypeScript and Phaser.
The File That Wouldn’t Open
In 2014 I wrote a tile-based dungeon crawler for a C# course. WinForms, .NET Framework 3.5, arrow-key movement around a dungeon of rooms and halls. You picked up items, equipped a weapon, and fought enemies that chased you when you got close. There was an Ant and, for reasons I no longer remember, a Cthulhu.
When I dug the project out years later, it didn’t run. Not “runs with warnings.” It crashed on launch. The reason turned out to be instructive: the game stored its inventory, items, and creatures in a SQL Server database file (an .mdf, wired up through typed DataSets), and that file wasn’t in the preserved copy. On top of that, the whole thing targeted a toolchain that no longer exists on my machine: .NET Framework 3.5 and Visual Studio 2010.
So I had the same decision I had with my old physics simulations: preserve the original exactly, and build something modern beside it. This time I built two modern versions, because the interesting question wasn’t just “can I make it run again” but “what would I do differently if I built this today, for real.”
Three Tracks, One Lineage
The repo ended up with three parallel tracks:
original/: the 2014 code, untouched. The historical record, crashes and all.extensions/csharp-winforms/: a faithful .NET 8 WinForms port. Same game, same lineage, running on a modern machine.extensions/web/: a from-scratch rewrite in TypeScript and Phaser 3. The souped-up version.
The two rewrites deliberately share no code. They’re in different languages targeting different platforms, and trying to force a shared core between a Windows desktop app and a browser game would have made both worse. What they share is lineage: the tile-room idea from the original, and the world and combat model from a separate project of mine, an Evennia space MUD.
Track One: Getting the Original Running Again
The port’s job was parity, not reinvention. Same dungeon, same enemies, same stats window and event log. The work was mostly surgical:
Rip out the database. The original loaded its dungeon layout, treasure placements, player inventory, and enemy roster from SQL Server. There was even a commented-out block in the source showing where everything went before it got moved into the database. I used that block as a reference and turned the whole thing into a plain SeedData class, all defined in code: the dungeon, the locked door and its key, the treasure, the starting inventory, the Ant and the Cthulhu. No database, no startup crash.
Fix the tile loading. The original loaded its tilesheet with a hardcoded relative path (../../tiles32.png) that only worked from a specific build directory. I made the image a project resource copied to the output folder and loaded it relative to the executable.
Fix a movement bug. The original had a subtle bug where walking into a null cell would set the player’s position to itself. That ate the input but looked like nothing happened. The port treats out-of-bounds and null cells as blocked, which is what you’d expect.
Separate logic from UI. The 2014 version mixed game rules into the WinForms code, including a static event for combat that would cross-talk between games. I split it into a RobotGame.Core library with no WinForms dependency and a thin UI on top, which made the whole thing unit-testable. There’s now an xUnit suite covering map construction, movement rules, and combat math.
The result builds with dotnet build and launches with dotnet run. It’s the same game I wrote as a student, minus the crash.
Track Two: What I’d Build Today
The web version is where I let myself reimagine things. Same bones (tile rooms, bump-into-enemies combat, pick stuff up), but rebuilt as a sci-fi browser game themed to match my space MUD. You play a salvage robot exploring a derelict station. The MUD’s vocabulary carries over: your health is integrity, your attack is processing, your defense is shielding.

The single rule I held myself to was this: all the game logic lives in plain TypeScript with no Phaser or DOM imports. Phaser only draws the state and forwards input. That one constraint paid for itself several times over.
It made the game testable. The room-graph layout, movement and collision, fog-of-war tracking, combat damage, defeat and respawn, progression math, mining, crafting, save/load, and (now) the whole story arc all run headless under Vitest with no browser. There are 82 of those tests now, and they run in well under a second. Every new system went in as pure logic with tests before it ever touched a sprite, which is the main reason the thing hasn’t turned into spaghetti.
It made the game scriptable. Because the logic is decoupled, I could put a small agent API on top of it: a reset() / step() loop that hands out a serializable observation and takes a discrete action. An AI or a script can play the game deterministically, where the same seed and same actions produce the same run every time. There are two example agents (a random one and a simple heuristic that chases and attacks hostiles), and in a dev build you can reach in and drive the live on-screen game from the browser console.
It left the door open for multiplayer. The world holds authoritative state, player actions are serializable commands, and each room is its own state partition. None of that is networked yet, but the shape matches what a Colyseus server would want, so adding a second player later isn’t a rewrite.
On top of that core, the web version has grown well past the original: a camera that follows you room to room with fog of war, real-time combat with enemies that pursue you (and bell-curve damage so hits aren’t a flat number), a mining mechanic where you break down ten kinds of junk into themed materials, a data-driven crafting system (find recipe chips in rooms, then build batteries, armor, better weapons, a faster mining laser), an XP/level/skill/profession progression lifted from the MUD, save/load to the browser, and a HUD with inventory, crafting, and controls panels.
The Map Is a Graph Projected Onto a Grid
One piece I’m happy with is how the station is laid out. The world is 15 rooms across two zones, defined as pure data: rooms are nodes, and exits are directed N/S/E/W edges between them (transcribed from my space MUD’s room/exit data). That’s a labeled directed graph, not a grid.
But the game renders on a grid, where north has to be up and walking north then south has to bring you back. So a layout solver walks the graph breadth-first from the start room and assigns each node integer (x, y) coordinates by following the direction of each edge. The MUD’s graph isn’t guaranteed to be a clean planar grid (two different rooms can end up wanting the same coordinate), so the solver resolves conflicts deterministically, with first assignment winning, and logs a warning instead of silently overlapping rooms. The upshot: I author the world as a simple list of rooms and doorways, and the spatial map falls out of it automatically.
Each room then furnishes itself to match its name. The Observation Deck gets telescopes and viewports, the Data Core gets server racks, the Reactor Terminal gets a glowing core, Crew Remnants gets bunks, all scattered deterministically from a seed so a given world is always the same.
The Audio Is Its Own Story
I wanted a chiptune soundtrack in the spirit of late-1980s Sunsoft NES games, the kind with driving triangle bass under a syncopated square lead. The important word there is spirit. I didn’t want to ship a single byte of anyone else’s music. No ripped MIDI, no NSF dumps, nothing.
So the audio is synthesized from scratch at runtime with the Web Audio API. There’s a tiny PSG-style voice set (square and pulse leads, a triangle bass, a noise channel for percussion), and the theme is written out as note patterns in two variants: a calmer exploration mix and a busier combat mix that shares the same harmony, so they cross-fade cleanly when a hostile walks into your room. There’s a real beat under it (kick/snare/hat) and the harmony follows an actual chord progression rather than me poking notes until it sounded okay. The sound effects (move, hit, damage, defeat, pickup, level-up) are all little generated envelopes over the same voices. It’s an original composition that only references the style of the era, which is the only honest way to do an homage.
The Art Became Its Own Thing
I started with a free CC0 tileset (Kenney’s “Sci-Fi RTS”) to get moving, and it worked, but the game looked like every other asset-pack prototype. So I replaced all of it with original art in the neon-on-dark robot style I use elsewhere on this site. Everything is now hand-authored SVG: the station tiles (three floors, four walls, three doors), ten kinds of mineable junk, the room-themed furniture, the item icons, five hostile robot types, and the player bots.
The game even opens on a “pick your bot” screen, and the player chassis are the same neon robots that greet you around imadestuff.com, dropped straight into the game. Most of the non-player art is generated by a small Python script that writes the SVGs, so retuning a look is a code edit rather than a pixel-pushing session. The later story pass just added a few more icons (key cards, ship components, install markers, projectiles) to the same script. Everything is original and carries no license strings; every asset and tool is recorded in the repo’s ASSETS.md.
Giving It a Reason to Exist
The sandbox was fun to wander, but it had no point. No goal, no ending, no stakes. So the next pass gave it a story: Restore the Ship. You wake up as a salvage robot on a derelict station whose own core AI tore the ship’s systems apart and scattered them “for safekeeping.” Your job is to recover the seven ship components, reinstall each one, and fly home to Earth. After the credits roll you’re dropped back into the station to free-roam.
That one framing pulled a dozen systems in behind it, and the pure-logic rule kept paying off. Every one of them went in as tested Core code first:
- Seven missions, one loop. Each component is held by a guardian, recovered by defeating it, and installed at a thematically matching room (reactor core goes in the Reactor Terminal, nav array on the Observation Deck). A story state machine (
intro,restoring,finalBoss,departed,freeRoam) tracks progress and drives the HUD’s “N/7 installed” objective. - Eight bosses. Seven guardians, one per component, that never respawn and drop their component on defeat. Then a single massive station-core boss that only wakes up once all seven are installed, with a nastier second phase at half health. It’s deliberately tuned to be very hard without several crafted upgrades, so the economy and crafting systems finally have teeth.
- Key cards that make sense. Some rooms are locked. The trick I like: the lock belongs to the room, not the door, so every entrance into a locked room shares one key. A build-time check walks the key graph to guarantee the station is always solvable, so no key is ever stranded behind the door it opens.
- Combat with actual decisions. A dash with invulnerability frames for dodging, a ranged attack you unlock by crafting the right weapon, enemy attacks that telegraph before they land, and knockback. Different enemy types now run different brains: stationary sentries, drifting drones, scuttling crawlers, relentless hunters.
- Texture around the loop. A single credits-only vendor (distinct from the material-based fabricator), four chassis that are balanced but flavored so your pick changes how the run feels (the fat Barrel Bot hits hard but lumbers; the Spindle Walker is fast and fragile), a station minimap that marks points of interest, and discoverable crew logs that tell you, piece by piece, what the AI did.
None of this touched the rendering contract. The story machine, the missions, the bosses, the key graph, dash and ranged resolution, the vendor math: all of it is plain TypeScript with tests, and Phaser just draws the result. Adding a win condition to a game that never had one turned out to be mostly a data-and-logic exercise, which is exactly what the architecture was supposed to buy me.
What I Took Away
The thing I keep relearning is that the design doesn’t change nearly as much as the engineering around it. The 2014 game and the 2026 web game are, at their core, the same idea: walk around tile rooms, fight things, get stronger. The equations of combat are basically identical. What changed is everything surrounding that idea. The state is decoupled from rendering, the logic is tested, the randomness is seeded and deterministic, and the whole thing can be driven by a script.
Building the port taught me what the original was. Building the web version taught me what I’d do with the same idea now. Keeping the original untouched in between meant I never had to choose between the two.
Where it stands today: the web game is about 6,400 lines of TypeScript (plus 1,000 lines of headless tests, 82 of them) and the WinForms port another ~1,400 lines of C#. The loop of explore, fight, mine, craft, level up now has a spine running through it: recover the seven components, beat the thing that scattered them, fly home. When I wrote the first version of this post the sandbox had no reason to exist. Building that reason was the thing I said I was looking forward to, and the architecture held up exactly the way I hoped. The next itch is post-departure free-roam content and, someday, the multiplayer the Core was always shaped for.
Play it in your browser · Source code: Robot-Game on GitHub