<?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: emick</title>
    <description>The latest articles on Forem by emick (@emick).</description>
    <link>https://forem.com/emick</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%2F1052699%2F81076bc0-1375-4b09-8342-13ada6e80504.jpeg</url>
      <title>Forem: emick</title>
      <link>https://forem.com/emick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/emick"/>
    <language>en</language>
    <item>
      <title>Cloning a 90s DOS Game with AI</title>
      <dc:creator>emick</dc:creator>
      <pubDate>Sun, 08 Feb 2026 13:05:40 +0000</pubDate>
      <link>https://forem.com/emick/cloning-a-90s-dos-game-with-ai-3fe7</link>
      <guid>https://forem.com/emick/cloning-a-90s-dos-game-with-ai-3fe7</guid>
      <description>&lt;p&gt;Since I've been running a lot of AI-agent experiments lately, I wanted to see how far I could get building a game just by using prompts alone (aka &lt;em&gt;vibe coding&lt;/em&gt;). For this experiment I used OpenAI &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex CLI&lt;/a&gt; with the GPT-5.2 Codex model (high thinking mode).&lt;/p&gt;

&lt;p&gt;I didn't have a fresh game idea ready, so I decided to clone something small and nostalgic: &lt;a href="https://dosgames.com/game/paddlers/" rel="noopener noreferrer"&gt;Paddlers&lt;/a&gt;, a 1996 indie DOS game that mixes Arkanoid-like gameplay with four-player chaos. The goal wasn't a perfect 1:1 remake—just something close enough to feel like the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Point: Treat the .exe as a Data Container
&lt;/h2&gt;

&lt;p&gt;I wanted to use the original game's levels so I pulled a trick I have used in the past. I gave Codex the &lt;em&gt;original DOS executable&lt;/em&gt; (only 80 kB) and let it dig through the executable to find the level data. &lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Level Data
&lt;/h2&gt;

&lt;p&gt;AI seems to have a great eye for patterns. I gave a couple of observations as a starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are &lt;strong&gt;10 stages&lt;/strong&gt; in the game&lt;/li&gt;
&lt;li&gt;The first stage is a &lt;strong&gt;5×5 grid&lt;/strong&gt;, centered&lt;/li&gt;
&lt;li&gt;The full game grid seems to be &lt;strong&gt;11×11&lt;/strong&gt; blocks&lt;/li&gt;
&lt;li&gt;Most stages include sparse patterns (not fully filled)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After ~40 seconds, Codex had already found the level data. Codex’s reasoning summary revealed that the breakthrough came by looking for a contiguous region of low-value bytes that resemble tile IDs. Needless to say, I was already impressed. A likely level region showed up around offset &lt;code&gt;0x15FEB&lt;/code&gt;, and decoding it produced a perfect match for stage 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000000
00000000000
00000000000
00011111000
00017331000
00013131000
00013371000
00011111000
00000000000
00000000000
00000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Level 1 image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3niclc4hwf8ky2uinty2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3niclc4hwf8ky2uinty2.png" alt="Level 1 screenshot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was still a little puzzled, since both special blocks had code 7 in the level data. That seemed strange. Nevertheless, I asked Codex to export the levels to files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting Graphics: AI Being Lazy AND Creative
&lt;/h2&gt;

&lt;p&gt;Next were the block graphics. From observation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;blocks appear about &lt;strong&gt;30×30&lt;/strong&gt; pixels&lt;/li&gt;
&lt;li&gt;there's a small 2 px margin between blocks in the grid&lt;/li&gt;
&lt;li&gt;code &lt;strong&gt;1&lt;/strong&gt; is bluish, &lt;strong&gt;3&lt;/strong&gt; turquoise/cyan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex had some trouble with extracting the graphics and I did not push it much. But it found a creative workaround: it copied the common block graphic from a screenshot I had given to it and figured out that the block colors are exactly the same as EGA color codes (blue = 1, cyan = 3, ...). I would not have noticed that!&lt;/p&gt;

&lt;p&gt;So Codex extracted one block and produced recolorings using the EGA palette. It wasn't the "purest" method, but it let me go forward with the game implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quest for the Game Logic
&lt;/h2&gt;

&lt;p&gt;To replicate mechanics properly, I needed rules: scoring, special blocks, level progression, ball behavior. Since figuring out that by myself sounded like a lot of work I thought I could try to see if I could extract at least some of the logic through decompiling the binary to C code.&lt;/p&gt;

&lt;p&gt;I decompiled with both &lt;strong&gt;Ghidra&lt;/strong&gt; and &lt;strong&gt;Reko&lt;/strong&gt;, which resulted in vastly different outputs: one contained "recognizable gameplay logic" (in AI's words, not mine); the other "mostly looked like runtime glue".&lt;/p&gt;

&lt;p&gt;To give an impression of what decompiled DOS code feels like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FUN_1000_010a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcVar1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;cVar2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;undefined2&lt;/span&gt; &lt;span class="n"&gt;in_CX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;undefined2&lt;/span&gt; &lt;span class="n"&gt;extraout_DX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;undefined1&lt;/span&gt; &lt;span class="n"&gt;in_CF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;pcVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;swi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x21&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcVar1&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;in_CF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pcVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;swi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x21&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcVar1&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;in_CF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;pcVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;swi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;cVar2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcVar1&lt;/span&gt;&lt;span class="p"&gt;)();&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cVar2&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;DAT_0000_0470&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;DAT_256b_0096&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extraout_DX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;DAT_256b_0098&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in_CX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;FUN_1000_021c&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="n"&gt;FUN_1000_4881&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="n"&gt;FUN_1000_c388&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="n"&gt;FUN_1000_0260&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1934&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;FUN_1000_02a9&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the rest of it looks just as incomprehensible: unnamed variables, interrupt calls, anonymous functions, and artifacts of 16-bit DOS conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Clear Signal: Scoring and Special Blocks
&lt;/h2&gt;

&lt;p&gt;But luckily that "incomprehensible mess" was not so hard for Codex. It managed to cut through and find some of the game logic I was looking for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I dug through the decompiles; the Ghidra output has the useful gameplay bits. Here's what I can confidently extract from the Ghidra C:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Score updates + cap:&lt;/strong&gt; &lt;code&gt;FUN_1000_5751&lt;/code&gt; adds points to a player and caps the score at 30000, then updates the HUD. &lt;code&gt;paddlers-ghidra.c:4576&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block hit scoring:&lt;/strong&gt; When a block is removed, it always awards &lt;strong&gt;+10&lt;/strong&gt; points; special blocks can award &lt;strong&gt;+100&lt;/strong&gt;. The block grid is indexed with &lt;code&gt;row * 0xb + col&lt;/code&gt; → 11 columns, matching the 11×11 level size. &lt;code&gt;paddlers-ghidra.c:3342&lt;/code&gt; &lt;code&gt;paddlers-ghidra.c:3368&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Special block IDs:&lt;/strong&gt; If a block value is &lt;code&gt;&amp;gt; 7&lt;/code&gt;, it triggers special behavior; cases &lt;code&gt;8, 9, 10, 0xb&lt;/code&gt; branch to different handlers, with &lt;code&gt;0xb&lt;/code&gt; explicitly giving &lt;strong&gt;+100&lt;/strong&gt; points. &lt;code&gt;paddlers-ghidra.c:3344&lt;/code&gt; &lt;code&gt;paddlers-ghidra.c:3349&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bonus per survivor:&lt;/strong&gt; Stage-clear bonus is computed as &lt;code&gt;300 / survivors&lt;/code&gt; and awarded to each surviving player. That yields 75/100/150/300 for 4/3/2/1 survivors, matching the readme. &lt;code&gt;paddlers-ghidra.c:3496&lt;/code&gt; &lt;code&gt;paddlers-ghidra.c:3511&lt;/code&gt; &lt;code&gt;paddlers-ghidra.c:3518&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage block counter:&lt;/strong&gt; &lt;code&gt;DAT_256b_19f0&lt;/code&gt; is decremented on each block removal (likely remaining block count). &lt;code&gt;paddlers-ghidra.c:3363&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Mystery of Block Code 7 Solved
&lt;/h2&gt;

&lt;p&gt;When I extracted the levels, one thing baffled me. Every special block showed up as a &lt;code&gt;7&lt;/code&gt;. That seemed wrong until the decompile and discussion with Codex clarified the design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the level data stores only &lt;strong&gt;0–7&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 is a placeholder&lt;/strong&gt; that is replaced during level initialization with a random special block&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building the Clone
&lt;/h2&gt;

&lt;p&gt;I initially considered &lt;a href="https://www.pygame.org" rel="noopener noreferrer"&gt;Pygame&lt;/a&gt;, but a quick discussion with ChatGPT convinced me that &lt;a href="https://godotengine.org/" rel="noopener noreferrer"&gt;Godot&lt;/a&gt; would be a better, more portable option for my case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Godot Reality Check: Parse Errors and Type Inference
&lt;/h2&gt;

&lt;p&gt;The initial implementation by Codex threw parse errors that stopped scripts from loading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a custom &lt;code&gt;get_rect()&lt;/code&gt; collided with &lt;code&gt;Sprite2D&lt;/code&gt; native method naming (warnings treated as errors)&lt;/li&gt;
&lt;li&gt;type inference failed for locals like &lt;code&gt;prev_pos&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;rect&lt;/code&gt;, &lt;code&gt;offset&lt;/code&gt;, &lt;code&gt;angle&lt;/code&gt;, plus later geometry variables like &lt;code&gt;half_len&lt;/code&gt;, &lt;code&gt;min_x/max_x&lt;/code&gt;, &lt;code&gt;min_y/max_y&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;some inferred values became &lt;code&gt;Variant&lt;/code&gt;, which also triggered warnings-as-errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex was able to fix those in one pass. Now the game starts in Godot, but the result was a minor letdown: most of the original game logic was not implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconstructing Missing Mechanics
&lt;/h2&gt;

&lt;p&gt;Apparently some of the mechanics were hard to locate in the decompile. So I just asked the Codex to implement those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ball spawns in corners/corner-ish positions aimed toward center&lt;/li&gt;
&lt;li&gt;players have distinct colors&lt;/li&gt;
&lt;li&gt;balls spawn neutral gray and don't award points to anyone&lt;/li&gt;
&lt;li&gt;only after a paddle touch do balls become "owned" and adopt player color&lt;/li&gt;
&lt;li&gt;lives reset to 3 each level&lt;/li&gt;
&lt;li&gt;the level ends when all players are dead&lt;/li&gt;
&lt;li&gt;icon overlays for all specials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex got all of it right on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result: A Playable Clone
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/4IuoW-PlY4g"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;And here's a link to the original &lt;a href="https://dosgames.com/game/paddlers/" rel="noopener noreferrer"&gt;Paddlers&lt;/a&gt;, which is playable in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Experiment Demonstrated
&lt;/h2&gt;

&lt;p&gt;In one evening and 16 prompts, I extracted all 10 levels, reproduced the core scoring rules, rebuilt several mechanics, approximated the block art, and ended up with a playable clone that feels surprisingly close.&lt;/p&gt;

&lt;p&gt;During the process, Codex surprised me three times:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It managed to scrape the levels from raw binary data.&lt;/li&gt;
&lt;li&gt;It figured out the blocks were colored using EGA color codes.&lt;/li&gt;
&lt;li&gt;It reconstructed the entire scoring logic from decompiled code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Overall, this suggests that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developing simple games by vibe coding (without looking at the resulting code) is clearly possible, even easy.&lt;/li&gt;
&lt;li&gt;AI is highly effective at finding structures even inside binaries.&lt;/li&gt;
&lt;li&gt;Although decompiled code is messy, AI can still pinpoint and extract a lot of game logic from it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;At this point, I felt the experiment was complete. I wasn’t interested in finishing the game further or publishing it, since that would raise obvious copyright issues—and the original is already playable in &lt;a href="https://www.dosbox.com/" rel="noopener noreferrer"&gt;DosBox&lt;/a&gt; and in the &lt;a href="https://dosgames.com/game/paddlers/" rel="noopener noreferrer"&gt;browser&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>reverseengineering</category>
    </item>
  </channel>
</rss>
