DEV Community

Cover image for I Built a Dungeon Crawler Game in Ruby (And It Actually Works)
David Silva
David Silva

Posted on • Originally published at davidslv.uk

I Built a Dungeon Crawler Game in Ruby (And It Actually Works)

When I tell people I'm building a game in Ruby, I get looks.

"Ruby? For a game? Isn't that... slow?"

Fair question. Most game developers reach for C++, C#, or Unity. Ruby is for web apps, not games. Everyone knows this.

Except I've been building a roguelike – a procedurally generated dungeon crawler inspired by the 1980s classic Rogue – in Ruby for six months now, and it's been brilliant.

What I'm Building

Think classic dungeon crawler: ASCII graphics, procedurally generated mazes, turn-based movement, permadeath. Your character (@) navigates randomly generated dungeons, fighting monsters, collecting items, trying not to die.

It runs entirely in the terminal. No fancy graphics. Just pure game logic and procedural generation.

The Unconventional Choice

I'm not a game developer by training. I build web applications and APIs. So choosing Ruby for a game felt both natural and slightly mad.

But here's the thing: I wasn't building a AAA title. I was building a turn-based game that runs in the terminal. My bottleneck wasn't performance. It was understanding game architecture.

For that, Ruby's clarity was perfect.

Rapid Iteration Changed Everything

No compilation step. No waiting. Just change the code and run it.

I implemented four different maze generation algorithms: Binary Tree, Aldous-Broder, Recursive Backtracker, and Recursive Division. Here's how simple the Binary Tree algorithm looks:

class BinaryTree < AbstractAlgorithm
  def self.on(grid)
    grid.each_cell do |cell|
      has_north = !cell.north.nil?
      has_east = !cell.east.nil?

      if has_north && has_east
        cell.link(cell: rand(2) == 0 ? cell.north : cell.east, bidirectional: true)
      elsif has_north
        cell.link(cell: cell.north, bidirectional: true)
      elsif has_east
        cell.link(cell: cell.east, bidirectional: true)
      end
    end

    grid
  end
end
Enter fullscreen mode Exit fullscreen mode

Look at that. grid.each_cell reads like English. The logic is clear: randomly connect each cell either north or east. I could tweak this, run it, see results immediately. No fuss.

When you're learning game architecture – which I was – this feedback loop is invaluable.

Components Stay Simple

The core of my game uses Entity-Component-System (ECS) architecture. Components should be pure data containers, and Ruby makes this trivial:

class PositionComponent < Component
  attr_reader :row, :column

  def initialize(row:, column:)
    super()
    @row = row
    @column = column
  end

  def set_position(row, column)
    @row = row
    @column = column
  end
end
Enter fullscreen mode Exit fullscreen mode

That's my entire PositionComponent. No boilerplate. No getters and setters cluttering the code. Ruby's attr_reader handles it. Named parameters make it obvious what you're passing:

position = PositionComponent.new(row: 5, column: 10)
Enter fullscreen mode Exit fullscreen mode

Compare this to languages where you need builder patterns just to maintain readability. Ruby gets out of your way.

What About Performance?

Let's be honest: Ruby isn't fast.

For my use case? Didn't matter. The game is turn-based. It waits for player input. The bottleneck is human reaction time, not Ruby's execution speed.

Maze generation on an 80×40 grid? Milliseconds. Entity queries with dozens of entities? Trivial. If I were building a real-time action game with hundreds of entities updating 60 times per second, Ruby would be the wrong choice.

But I wasn't. Context matters.

Developer Joy Matters Too

Here's what I didn't expect: Ruby made me happy while coding.

When I write entity.has_component?(:position), it reads like a question I'd ask a colleague. The code communicates intent clearly. I could focus on understanding ECS, event systems, and procedural generation rather than fighting with syntax.

When you're building something complex in your spare time, enjoyment matters. If I'd chosen a language I found frustrating, I might have abandoned the project.

The Result

The game now has procedurally generated mazes, an Entity-Component-System architecture, event-driven logging, command-based input handling, and 48 spec files of test coverage.

Not bad for a language supposedly "not meant for games."


Want the full story? I wrote a comprehensive article on my blog covering the complete architecture, testing strategy, and lessons learned: Why I Chose Ruby to Build a Roguelike Game

The code is open source on GitHub if you want to see how it all fits together.

P.S. – I documented this entire journey in a book: Building Your Own Roguelike: A Practical Guide. It walks through building this from scratch – the ECS pattern, event systems, maze generation algorithms, and everything you see in Vanilla Roguelike.

Thank you for reading,
David Silva

Top comments (0)