<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Hazel Granados — Blog</title>
    <link>https://www.hazeliscoding.dev/blog</link>
    <description>Notes on building software, by Hazel Granados.</description>
    <language>en-us</language>
    <lastBuildDate>Sun, 18 Oct 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.hazeliscoding.dev/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Digging a 2003 PS2 minigame out of a 2021 Unity remaster</title>
      <link>https://www.hazeliscoding.dev/blog/a-ps2-minigame-in-a-unity-remaster</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/a-ps2-minigame-in-a-unity-remaster</guid>
      <pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate>
      <description>Rebuilding Puzzle Boy from Shin Megami Tensei III Nocturne as a standalone game, from the game&apos;s own code but without shipping a byte of it. Part 1 of 3.</description>
      <category>reverse-engineering</category>
      <category>puzzle-boy</category>
      <category>claude-code</category>
      <category>ghidra</category>
      <category>csharp</category>
      <category>monogame</category>
      <content:encoded><![CDATA[<p>The method names in <em>Shin Megami Tensei III Nocturne HD Remaster</em> read like a PS2 source tree from 2003:</p>
<pre><code class="language-text">pbGame.pushCheck      pushing a block
pbGame.roundCheck     turning a swivel block
pbGame.hallCheck      falling into a hole (yes, &quot;hall&quot;)
pb_H                  the old pb.h header, now a C# namespace
pbEdit                the developers&#39; level editor, still in the build
</code></pre>
<p>That&#39;s Puzzle Boy, a minigame deep inside Nocturne. You guide Jack-o&#39;-Lantern through a maze by pushing blocks, turning swivel blocks and filling holes, while the Manikin Kid taunts you. It&#39;s Atlus&#39;s old Game Boy puzzler (Kwirk in the West), redone in 3D. I wanted it as a game of its own that runs anywhere, so I rebuilt it: <a href="https://github.com/hazeliscoding/puzzle-boy">puzzle-boy</a>.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/tour.webp" alt="Jack-o'-Lantern turns the view, switches to the overhead camera and undoes a push from the menu, in the port"></p>
<p>This is part 1 of 3. It covers the first two days: reading the rules out of the game, the line I drew around its copy protection, and how the rules became tests before they became code. Part 2 is about the stages the original never plays, and how one repo became three.</p>
<h2>The rules I started with</h2>
<p>Before writing any code, I wrote down what the project would never do, in the repo&#39;s <code>AGENTS.md</code>, so every agent session starts with it:</p>
<ul>
<li><strong>No Atlus data in the repo.</strong> No stages, text, audio, textures, models, metadata dumps or decompiler output. The game reads all of it from your own install at startup.</li>
<li><strong>No decompiled code in the source.</strong> Every rule found in the game becomes a test on a handmade stage first, and the code is written from the test.</li>
<li><strong>No circumvention.</strong> More on that below.</li>
</ul>
<p>I built it with Claude Code doing the reverse engineering through <a href="https://rea.tools">REA</a> and Ghidra, and me deciding.</p>
<h2>The metadata talks first</h2>
<p>The HD Remaster is a Unity 2019 game compiled with IL2CPP: C# turned into a 383 MB native <code>GameAssembly.dll</code>. IL2CPP keeps every class, method and field name in a separate file, <code>global-metadata.dat</code>, and Nocturne&#39;s isn&#39;t encrypted.</p>
<p>So before decompiling anything, a small parser read 5,378 type definitions. The Puzzle Boy classes were all there, with the names from the list above. <code>pbGame</code> alone has 74 fields and 60 methods. Nearby were <code>sdfChipFreeDelayed</code>, <code>dds3PrimFrameRect</code>, and <code>mcSave</code> for the PS2 memory card. The remaster is the 2003 C code, converted to C# and kept nearly as it was. The help text still documents the developers&#39; edit mode, translated into eight languages, and the debug menu has a &quot;PUZZLE BOY GAME&quot; entry next to &quot;MODEL VIEWER&quot;.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/manikin-kid.jpg" alt="The Manikin Kid explains the goal before stage 1: &quot;Do you see that spinning blue cube? That's the goal.&quot;"></p>
<h2>Six files broken by a line ending</h2>
<p>The stages are 50 files in a Unity bundle called <code>pazzleboy</code> (Atlus&#39;s typo). Each is 828 bytes: a character count, four start positions, a goal, then a 20×20 grid of 16-bit cells. <code>0x2000</code> is a wall, <code>0x0800</code> a hole, <code>0x60nn</code> part of block group <code>nn</code>, and swivels have their own pair of types.</p>
<p>Six files were a few bytes longer. In one of them, the character starts at y = 2573 on a 20-row grid.</p>
<p>Each of the six had exactly as many <code>0D 0A</code> byte pairs as extra bytes. Somewhere in Atlus&#39;s pipeline, a line-ending conversion treated these binary files as text and turned every <code>0A</code> byte into the Windows <code>0D 0A</code>. Undo it, and every file is 828 bytes again and makes sense. The broken one starts its character at (17, 10).</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/first-render-stage-44.jpg" alt="The port's first render: stage file 44, one of the six damaged files, drawn as plain boxes after the repair"></p>
<p>The code later explained why nobody ever noticed: the original never loads those six files. The port repairs them anyway, and its <code>.gitattributes</code> marks stage files as binary, so the same thing can&#39;t happen to me.</p>
<h2>The line around Denuvo</h2>
<p><code>GameAssembly.dll</code> is protected by Denuvo Anti-Tamper. I wasn&#39;t going to dump the running game&#39;s memory, de-obfuscate anything or patch it. What made the project possible at all was one check: all 225 Puzzle Boy methods live in the unprotected <code>.text</code> section, along with 13,002 of the game&#39;s 13,028 methods. The protected code is a separate 316 MB section.</p>
<p>So the rule is short: read only unprotected code, statically. If a question leads into protected code, answer it by playing the real game instead.</p>
<p>The usual tools didn&#39;t make it easy. REA couldn&#39;t open Unity bundles and had no IL2CPP support. Il2CppDumper couldn&#39;t find IL2CPP&#39;s registration tables, because the protector had renamed the sections that hold them. Found by hand, the tables turned out to use the v24.3 layout while the metadata claimed v24.2, and the dumper crashed on that too. So a script of about 100 lines followed each method&#39;s token to its address and wrote 13,028 names into a seed file for Ghidra.</p>
<h2>Ghidra versus a 32 GB laptop</h2>
<p>Then Ghidra had to analyze 383 MB of native code. The first run died about 12 minutes in, when the agent&#39;s session ended and took its background job with it. The second was launched outside Claude Code&#39;s process tree. After 52 minutes, Java held 11.4 GB and the machine had 0.1 GB of RAM free. I stopped it.</p>
<p>The overnight run spent 6.5 hours looping through the protected section without finishing. Denuvo&#39;s code is virtualized, so there&#39;s nothing readable there anyway; Ghidra was just wading through it.</p>
<p>So I made a local analysis copy of the DLL with the protected sections emptied. <code>.text</code> stayed byte-identical, and every address still lined up with the seed file. Nothing was defeated and nothing ran: it just removed the protected code from what Ghidra looked at. The analysis finished in <strong>41 minutes</strong>.</p>
<p>One beat I like: the agent then tried to commit that trimming script to the public repo, and Claude Code&#39;s own safety check refused. A public script that strips sections from a Denuvo-protected binary reads as anti-DRM tooling, whatever it&#39;s for. I agreed. The script and the copy stayed on my machine, and the explanation went here instead.</p>
<h2>Reading the rules, with an adversary</h2>
<p>The research ran as one batch: seven agents each read one area of the decompiled code (pushing, swivels, the turn loop, stages, loading, undo, presentation), and for each one, a second agent re-checked every claim against the pseudocode and tried to prove it wrong. Then a critic looked for what was missing.</p>
<table>
<thead>
<tr>
<th>The research batch</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>Agents</td>
<td>15</td>
</tr>
<tr>
<td>Tool calls</td>
<td>1,055</td>
</tr>
<tr>
<td>Claims made</td>
<td>111</td>
</tr>
<tr>
<td>Confirmed</td>
<td>97</td>
</tr>
<tr>
<td>Refuted or corrected</td>
<td>10</td>
</tr>
<tr>
<td>Left uncertain</td>
<td>4</td>
</tr>
</tbody></table>
<p>The findings went into the public <code>ROADMAP.md</code> as plain-language rules, each with a short evidence ID, and never as pseudocode. A few surprises:</p>
<ul>
<li><strong>Only 21 of the 50 stage files are ever played.</strong> Level 1 is 20 stages in a fixed order. The six damaged files are among the unused ones.</li>
<li><strong>There&#39;s no move counter.</strong> The only number on screen is the stage counter.</li>
<li><strong>Undo keeps 30 steps and only records pushes and turns.</strong> Undoing throws away any walking since the last push.</li>
<li><strong>The original has a real bug:</strong> its character-collision check swaps x and y. It never matters, because every played stage has exactly one character.</li>
</ul>
<h2>Rules as tests</h2>
<p>Every rule then became a test on a stage drawn as text. Columns are x and rows are y, and a helper turns the drawing into real 828-byte stage bytes, so every test also goes through the parser:</p>
<pre><code class="language-text">#2##
1a..
#o##
#G##
</code></pre>
<p><code>#</code> is a wall, <code>.</code> floor, <code>o</code> a hole, <code>G</code> the goal, digits are characters and lowercase letters are block groups. The expected result is just another drawing.</p>
<p>To check the tests had teeth, I broke the engine on purpose nine ways: reverse the swivel&#39;s turning sense, skip the fall into a hole, drop the sweep check, set the undo limit to 31, and so on. Every mutation failed at least one test. Reversing the swivel sense failed 22.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/menu-undo.jpg" alt="The original's □ menu in the port, with UNDO taking back a push"></p>
<h2>2,756 inputs, and the board was upside down</h2>
<p>Tests prove the code matches what I read. They don&#39;t prove I read it right. For that I needed the real game&#39;s solutions.</p>
<p>AppMedia publishes the full input list for every stage of the HD version. Parsed into left, right, up and down and replayed in the port, they cleared all 20 stages: <strong>2,756 inputs, each stage clearing exactly on its last one, with none refused.</strong></p>
<p>Except at first, they didn&#39;t. Stage 1&#39;s solution only worked under one of the eight possible ways to map screen directions onto the grid, and it wasn&#39;t the port&#39;s. Gameplay video showed why: the original draws +y going <em>up</em> the screen. The port drew it down, so every stage was a mirror image and swivels seemed to turn the wrong way. The rules were right; only the picture was flipped. After a one-line fix, the same mapping cleared all 20.</p>
<h2>Measuring the clock from video</h2>
<p>The original runs one tick per rendered frame, so its speed depends on your frame rate. A 144 Hz monitor plays it faster. The code can&#39;t tell you what &quot;normal&quot; is.</p>
<p>Gameplay video can. In 60 fps footage, every move lasts a multiple of 8 frames, and stage 1&#39;s five-cell walk lasts exactly 40. That&#39;s 4 ticks a step at 30 ticks a second, which is what the port runs. Even the extra tick after a swivel turn shows up as a 2-frame pause.</p>
<h2>Making it look like the real thing</h2>
<p>The rest of the first two days went into the original&#39;s look, all read from the player&#39;s install at startup: the board textures (DXT1, decoded on the CPU), the backdrop, Jack-o&#39;-Lantern&#39;s skinned model and idle animation, the sound effects and music (FMOD banks with Vorbis inside), the HUD, the □ menu and the Manikin Kid&#39;s lines.</p>
<p>The game ships no font file. Its text exists only as TextMeshPro distance-field atlases, so the port runs TextMeshPro&#39;s shader math on the CPU to draw it.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/complete.jpg" alt="COMPLETE! in the port, after the last push of a stage"></p>
<p>Before polishing, a review compared the port&#39;s timing against the research and the footage and found 12 real differences. The overhead view turns in 12 ticks, not 9. The camera orbits a point a quarter cell below Jack. The next stage loads 35 ticks after the winning input. And the original reads no input at all while a move plays, so a key pressed then is lost, not queued.</p>
<h2>Keeping it honest</h2>
<p>&quot;No Atlus data in the repo&quot; is a rule an agent can break by accident, so CI checks it. A script inspects every tracked file by its first bytes for Unity bundles, FMOD sound banks, IL2CPP metadata and Atlus flow scripts. It also rejects stage files outside the tests and Ghidra&#39;s generated names in the source.</p>
<p>A check for an absence proves nothing until you see it fail, so it has a positive control that plants each kind in a scratch repo and expects a failure. I also opened a throwaway pull request with a 4-byte stage file in it. CI failed exactly where it should, and the PR was closed unmerged.</p>
<p>Two mistakes worth admitting. To take screenshots of other stages, the agent once sent keystrokes to the game, while Windows was keeping focus on whatever window I had open. And the first frame-capture tool played the game&#39;s music on my speakers. Both tools now run the game with no real input and no sound.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/stage-20.jpg" alt="Stage 20, the last stage, in the port"></p>
<h2>Where it ended up</h2>
<p><a href="https://github.com/hazeliscoding/puzzle-boy/releases/tag/v0.1.0">v0.1.0</a> shipped about 24 hours after the first question. It has Windows, Linux and macOS builds, all 20 stages, and the original&#39;s look, timing, sound, HUD, dialogue and menu, read from your own copy. You need <em>Shin Megami Tensei III Nocturne HD Remaster</em> to play it, and the repository holds none of it.</p>
<p>Then I looked at the 30 stage files the original never plays, and found that six of them can&#39;t be won as shipped. That&#39;s part 2.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Every trap is proven: building an agent skill you can trust</title>
      <link>https://www.hazeliscoding.dev/blog/every-trap-is-proven</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/every-trap-is-proven</guid>
      <pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate>
      <description>What a coding agent gets wrong porting SDL2 to SDL3, how I measured it, and the agent that searched the whole disk for headers.</description>
      <category>claude-code</category>
      <category>agent-skills</category>
      <category>sdl3</category>
      <category>evals</category>
      <category>c</category>
      <content:encoded><![CDATA[<p>This compiles against SDL3 without a single warning:</p>
<pre><code class="language-c">if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {  // SDL3 returns true on success
    SDL_Log(&quot;SDL_Init failed: %s&quot;, SDL_GetError());
    return 1;                                          // so this runs on every launch
}
</code></pre>
<p>In SDL2, <code>SDL_Init</code> returned 0 on success. In SDL3 it returns <code>true</code>. The line looks the same, the compiler is happy, and the game quits every time it starts.</p>
<p>SDL3 is full of changes like that. An audio stream now opens paused, so the game is silent. Textures filter linearly by default, so pixel art comes out blurry. A texture with an alpha channel now blends, so an emulator&#39;s screen can go invisible. SDL&#39;s rename scripts handle most of a port and the compiler catches most of the rest, but neither one catches code that still compiles and now means something else. Coding agents make exactly these mistakes, because most of the SDL code they learned from is SDL2.</p>
<p>So I built <a href="https://github.com/hazeliscoding/sdl3-porter">sdl3-porter</a>, a Claude Code plugin that ports C and C++ from SDL2 to SDL3 and catches those changes. This post is less about the plugin than about the question I kept asking while building it: how do I know it works?</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/every-trap-is-proven/port.gif" alt="Claude Code loads sdl3-porter, ports a small SDL2 game, builds and runs it, and reports each trap it fixed by file, line and trap id"></p>
<h2>What counts as a trap</h2>
<p>I gave myself a strict definition. A trap is a change that compiles against SDL3 with warnings as errors (<code>-Wall -Wextra -Werror</code> on GCC and Clang, <code>/W4 /WX</code> on MSVC) and then breaks at runtime. If any compiler warns about it, the compiler already catches it, and it goes in a reference file instead.</p>
<p>A trap also doesn&#39;t count until it&#39;s proven. Each one has three small C programs:</p>
<ul>
<li>the <strong>SDL2 original</strong>, which must pass against SDL 2.32;</li>
<li>the <strong>naive port</strong>, the code an agent would write, which must compile cleanly and then <strong>fail</strong>;</li>
<li>the <strong>fixed port</strong>, which must pass.</li>
</ul>
<p>CI builds all three and runs them headless on Windows, Linux and macOS, against SDL 3.4.18 and the oldest supported 3.2.0. Headless means no display, no sound card and no gamepad: the offscreen video driver, dummy audio, SDL&#39;s virtual joystick and the software renderer.</p>
<p>The naive port is the important one. It&#39;s the positive control. If it ever stops failing, the trap is no longer proven, and CI goes red. It also has to fail for the right reason: it must print one line that starts with the trap&#39;s id, like <code>bool-returns: SDL_Init reported failure</code>, so a setup error can&#39;t pass for the trap.</p>
<p>That rule threw out a lot of good ideas. Nintendo face buttons, rumble, exclusive fullscreen and high DPI all change in SDL3, but none of them can be proven without real hardware. They became 12 sections of guidance that the agent checks by reading the code. The 17 traps are only the changes I could make fail on demand.</p>
<p>Proving things headless had its own traps. The vsync fixture can&#39;t time frames, because SDL2&#39;s software renderer reports vsync as on and never actually waits. It checks the renderer&#39;s state instead.</p>
<h2>Measuring it</h2>
<p>A trap card tells the agent what to look for. That doesn&#39;t mean the agent finds it. For that I used evals: run Claude Code on an SDL2 program, let it port the code, then check the result.</p>
<p>Two rules made the scores worth believing.</p>
<p><strong>Don&#39;t port the fixture.</strong> The fixture&#39;s SDL2 original checks the exact behavior the trap is about, which points straight at the answer. Each eval ports a separate sample instead: a plausible little SDL2 program with a neutral name and no self-checks. One more case ports a whole game, where the agent has to find the traps itself.</p>
<p><strong>Every grader has to be able to fail.</strong> Most graders are a regular expression over the ported files that checks one trap fixed or one porting step done. A test runs every trap grader against a port made only with SDL&#39;s rename scripts, and fails if any of them passes it. A grader that can&#39;t fail measures nothing.</p>
<p>Each case runs five times with the skill and five times without it, on Sonnet 5 and Haiku 4.5. I also wrote down the rules for messy results before I had any. An infrastructure error gets one traced rerun, and a second error counts as measured. Hitting the turn limit is a real result. No threshold ever gets lowered. When a new Claude Code release shipped in the middle of a batch, I pinned the version and reran the cases that had mixed versions, so one table never mixes two versions.</p>
<h2>What the numbers say</h2>
<p><img src="https://www.hazeliscoding.dev/images/blog/every-trap-is-proven/scores.svg" alt="Eval scores with and without sdl3-porter for each case, on Sonnet 5 and Haiku 4.5"></p>
<p>Without the skill, Sonnet 5 already gets 10 of the 17 traps right every time. On the other 7 it scores between 0.00 and 0.47. With the skill it scores 1.00 on all of them.</p>
<p>The skill doesn&#39;t help much with what the model already knows. It helps with silent default changes, where the SDL2 code looks perfectly fine: textures that now blend, textures that now filter, audio that SDL2 started by itself and SDL3 doesn&#39;t. Sonnet scores 0.00 on all three of those without the skill.</p>
<p>Finding is harder than fixing. In an early run, <code>text-input-off</code> scored 1.00 without the skill as a small standalone sample. In the whole game, the same model missed it in 3 runs out of 3. One trap in a small file is easy to spot. One trap in a game is easy to miss.</p>
<p>The smaller model gains more. On the whole game, Haiku 4.5 went from 0.39 without the skill to 0.92 with it. It isn&#39;t perfect, though. It scores 0.40 on <code>window-mode-null</code>, where it keeps the old call behind a NULL check and calls that a fix, and 0.80 on <code>logical-scale-separate</code>. I shipped 1.0 with both listed as known gaps instead of running the evals until they looked better.</p>
<p>Without the skill, scores also move between runs. <code>linear-by-default</code> scored 0.33 in one run and 0.00 in the next. Five runs per case is a small sample, so I read the table as &quot;reliably fixed&quot; versus &quot;sometimes missed&quot;, not as exact percentages.</p>
<h2>The agent that searched the whole disk</h2>
<p>The first smoke run took 785 seconds with the skill and 43 without. The skill was making the agent much slower.</p>
<p>A traced rerun showed why. The agent spent 87 seconds on one command: <code>find /</code>, looking for <code>SDL_video.h</code> so it could check its port against the SDL3 headers. I added a line to the skill telling it not to search for headers. In the first full eval run, one run hit the 1,200-second timeout anyway. A traced rerun showed the agent running <code>find / -xdev -iname SDL3.h</code> for 120 seconds, even though the skill had told it not to.</p>
<p>The fix was to stop asking for the behavior and remove the reason for it. The skill now ships with the 66 SDL3 headers it needs, 2.3 MB of them. The slowest run dropped from 1,200 seconds to 342.</p>
<p>Then it happened again. After dogfooding, I added a step telling the agent to build the port where it could. Three runs took 746, 808 and 1,130 seconds. This time the agent was searching the disk for an <em>installed</em> SDL3, its library or its CMake files, to decide whether it could build. My rule only covered headers, so this search wasn&#39;t against it.</p>
<p>The rule that finally held was about the action, not the target: don&#39;t run <code>find</code> or <code>locate</code> outside the project, and ask the build instead, with <code>pkg-config --exists sdl3</code> or by configuring the project. In the same nine runs, the agent searched outside the project zero times, and the slowest run took 202 seconds.</p>
<p>A rule against one target invites a search for the next one. A rule about the action covers the next target too.</p>
<h2>Facts in the output beat rules in the docs</h2>
<p>For 1.0, I added a script, <code>find_traps.py</code>, that runs every trap&#39;s search over the project in one command. Writing it found a bug of its own: seven searches never matched pointer access such as <code>ev-&gt;gbutton.button</code>, so they had silently skipped some real code.</p>
<p>The script also prints facts next to its hits. When it sees an old SDL2 hint name, it prints &quot;removed in SDL3&quot; beside it. Before that, Haiku left the dead hint in the whole game in 5 runs out of 5. In a check after the change, it left it in 1.</p>
<p>Rules in the docs did worse. One trap card says in plain words that a NULL check is not a fix. Haiku read that card and then called a NULL check &quot;no change needed&quot;. The traces also showed why small models miss things: Haiku reads files with a line limit, 80 to 150 lines at a time, so a trap card near the bottom of a long reference file sometimes doesn&#39;t get read at all.</p>
<h2>Against the maintainers</h2>
<p>Evals use programs I wrote. To test against real code, I ported open-source projects starting from the commit just before their maintainers did their own SDL3 port, then compared the two ports.</p>
<p>An early comparison found a trap I didn&#39;t have. A Game Boy emulator&#39;s maintainer had fixed a texture that SDL3 now blends by default, which made the whole screen invisible. The agent&#39;s port had missed it. It became <code>blend-by-default</code>, the trap Sonnet scores 0.00 on without the skill.</p>
<p>The larger ports:</p>
<table>
<thead>
<tr>
<th>Program</th>
<th>Size</th>
<th>Cost</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Woof!, a Doom source port</td>
<td>about 165,000 lines</td>
<td>$15.34, 91 min</td>
<td>builds clean, plays a demo headless</td>
</tr>
<tr>
<td>scrcpy</td>
<td>about 31,000 lines</td>
<td>$12.44, 22 min</td>
<td>builds clean, unit tests pass</td>
</tr>
</tbody></table>
<p>Both had misses. In scrcpy, the agent&#39;s own search found the text input code, and then it never turned text input on. Both agents ran every search at once and skimmed past hits. Now the skill asks for a verdict on every section that has hits. Woof!&#39;s menu turned up a mouse-mapping bug in letterboxed windows, which became the 17th trap, <code>logical-scale-separate</code>. The maintainers&#39; own port has the same bug.</p>
<p>The agent also beat the maintainers twice. scrcpy&#39;s own port sent a loop index in gamepad events where SDL3 expects a joystick ID, and the agent&#39;s port sent the ID. Woof!&#39;s port kept the <code>__MACOSX__</code> macro, which SDL3 no longer defines, so some macOS-only code silently stopped building. The agent had renamed it to <code>SDL_PLATFORM_MACOS</code>. I reported that one as <a href="https://github.com/fabiangreffrath/woof/issues/2952">woof#2952</a>. A contributor opened a fix within hours, and I checked the fix&#39;s macOS build without a Mac, by comparing strings in the binaries.</p>
<p>Every dogfood report also overcounted. One listed 6 traps where there was 1. Later, the port I recorded for the README flagged <code>blend-by-default</code> on a sprite whose pixels were all opaque, because the card&#39;s wording allowed it. The agent follows what the card says, not what I meant, so the cards got stricter.</p>
<h2>How it was built</h2>
<p>I built sdl3-porter with Claude Code itself. The repository&#39;s <code>AGENTS.md</code> holds the rules from this post: no trap without a naive port that fails, cite SDL&#39;s migration guide for every trap, never lower a threshold, and test every check that looks for an absence against a case where it should fail. Most of what&#39;s above is what happened when those rules met the evidence.</p>
<h2>Try it</h2>
<p>In Claude Code:</p>
<pre><code class="language-text">/plugin marketplace add hazeliscoding/sdl3-porter
/plugin install sdl3-porter@sdl3-porter
</code></pre>
<p>Then open an SDL2 project and ask Claude Code to port it to SDL3. Commit first, because the port is meant to be reviewed as a diff. The <a href="https://github.com/hazeliscoding/sdl3-porter#readme">README</a> has the full eval table and the known gaps, and <a href="https://github.com/hazeliscoding/sdl3-porter/blob/main/ROADMAP.md">ROADMAP.md</a> records every decision, with its date.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hello, World!</title>
      <link>https://www.hazeliscoding.dev/blog/hello-world</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/hello-world</guid>
      <pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate>
      <description>My first post on this portfolio site.</description>
      <category>angular</category>
      <category>portfolio</category>
      <category>personal</category>
      <content:encoded><![CDATA[<p>I’ve wanted a place on my own domain to write short notes about what I’m learning, what I’m building, and what I’m thinking about in tech.</p>
<!-- ![Blog placeholder image](/images/blog/placeholder.svg) -->

<p>This blog lives directly inside my Angular app, using Markdown files that get turned into routes at build time.</p>
<h2>What I’m optimizing for</h2>
<ul>
<li>Owning my content</li>
<li>Keeping it simple</li>
<li>Great performance and SEO</li>
</ul>
<p>Thanks for reading — more posts coming soon.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
