Skip to content
Roshan Arun Kumar
All projects

ChamberCrawler3000

2021

A terminal roguelike in C++: five playable races, seven enemy types, a floor generator and combat, organised around a polymorphic object hierarchy rather than switch statements.

  • C++
  • Make

Source code unavailable at the University of Waterloo's request.

Play it

The full game, in your browser

Arrow keys or the number pad to move. Reach the stairs on all five floors.

Entering the dungeon…

The brief was a dungeon crawler, but the real exercise was object-oriented design. The dungeon holds one collection of Objects; everything in it — the player, enemies, gold, potions, walls, doors, stairs — descends from that single base, so the game loop can iterate the floor without ever asking what kind of thing it is looking at.

That decision is what keeps the code small. Combat calls a virtual attack, and a troll regenerating each turn or a vampire draining HP is the subclass's business, not the loop's. Potions are pure interfaces with one useItem method, so the six effects — attack and defence up or down, HP up or down — are six tiny classes instead of a branching statement that grows every time the game does.

Each of the five races is a Player subclass with its own stats and rules, and dragons guard their hoards with behaviour no other enemy has. Adding a race or an enemy means writing a class, not editing the engine — which is the entire point of the exercise, and the first time that clicked for me as something other than an abstract rule.

What it does

  • 38 classes across a single Object hierarchy — characters, terrain, items and gold
  • Potions and enemies behind pure virtual interfaces, so behaviour lives in subclasses
  • Five playable races and seven enemy types, each with distinct rules
  • Procedural spawning across five chambers, with dragons guarding their hoards
  • Ported to TypeScript in full — the same map file, spawn weights and combat maths

Problems worth writing down

My first pass reached for casts and type checks in the game loop, which meant every new enemy touched code that had nothing to do with it.

Pushing the behaviour down into virtual methods removed the branching entirely. The lasting lesson was that asking an object what it is usually means the hierarchy is wrong.

Porting it to the browser, I gave ordinary enemies a guaranteed hit and reserved the miss roll for dragons and merchants. The rules all tested green, but the game was unplayable: four of the five races won none of sixty simulated runs.

Every enemy in the C++ rolls rand() % 2 before dealing damage — there is no always-hits enemy, and that coin flip is most of what makes the game survivable. Fixing it took the Troll from 8 wins in 60 to 20, and gave every race except the Vampire a route through. It only surfaced because a bot played hundreds of full runs; no unit test on the rules would have caught it.