<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: David Silva</title>
    <description>The latest articles on Forem by David Silva (@davidslv).</description>
    <link>https://forem.com/davidslv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3624184%2F01c672e3-dd69-4325-9522-1e1501b91553.jpeg</url>
      <title>Forem: David Silva</title>
      <link>https://forem.com/davidslv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/davidslv"/>
    <language>en</language>
    <item>
      <title>I Built a Dungeon Crawler Game in Ruby (And It Actually Works)</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Sat, 22 Nov 2025 11:24:00 +0000</pubDate>
      <link>https://forem.com/davidslv/i-built-a-dungeon-crawler-game-in-ruby-and-it-actually-works-40lf</link>
      <guid>https://forem.com/davidslv/i-built-a-dungeon-crawler-game-in-ruby-and-it-actually-works-40lf</guid>
      <description>&lt;p&gt;When I tell people I'm building a game in Ruby, I get &lt;em&gt;looks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;"Ruby? For a game? Isn't that... slow?"&lt;/p&gt;

&lt;p&gt;Fair question. Most game developers reach for C++, C#, or Unity. Ruby is for web apps, not games. Everyone knows this.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What I'm Building
&lt;/h2&gt;

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

&lt;p&gt;It runs entirely in the terminal. No fancy graphics. Just pure game logic and procedural generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unconventional Choice
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For that, Ruby's clarity was perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rapid Iteration Changed Everything
&lt;/h2&gt;

&lt;p&gt;No compilation step. No waiting. Just change the code and run it.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BinaryTree&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;AbstractAlgorithm&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_cell&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;has_north&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="n"&gt;has_east&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_north&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;has_east&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;has_north&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;has_east&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;grid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that. &lt;code&gt;grid.each_cell&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;When you're learning game architecture – which I was – this feedback loop is invaluable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components Stay Simple
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PositionComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Component&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="vi"&gt;@row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="vi"&gt;@column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="vi"&gt;@column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PositionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;row: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to languages where you need builder patterns just to maintain readability. Ruby gets out of your way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Performance?
&lt;/h2&gt;

&lt;p&gt;Let's be honest: Ruby isn't fast.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But I wasn't. Context matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Joy Matters Too
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect: Ruby made me &lt;em&gt;happy&lt;/em&gt; while coding.&lt;/p&gt;

&lt;p&gt;When I write &lt;code&gt;entity.has_component?(:position)&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Not bad for a language supposedly "not meant for games."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Want the full story?&lt;/strong&gt; I wrote a comprehensive article on my blog covering the complete architecture, testing strategy, and lessons learned: &lt;a href="https://davidslv.uk/ruby/game-development/2025/11/22/why-ruby-roguelike.html" rel="noopener noreferrer"&gt;Why I Chose Ruby to Build a Roguelike Game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is &lt;a href="https://github.com/Davidslv/vanilla-roguelike" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt; if you want to see how it all fits together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; – I documented this entire journey in a book: &lt;a href="https://www.amazon.com/Building-Your-Own-Roguelike-Hands-ebook/dp/B0G1RBWF6V" rel="noopener noreferrer"&gt;Building Your Own Roguelike: A Practical Guide&lt;/a&gt;. It walks through building this from scratch – the ECS pattern, event systems, maze generation algorithms, and everything you see in Vanilla Roguelike.&lt;/p&gt;

&lt;p&gt;Thank you for reading,&lt;br&gt;
David Silva&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>gamedev</category>
      <category>roguelike</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
