<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://tonym128.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tonym128.github.io/" rel="alternate" type="text/html" /><updated>2026-10-30T03:24:12+00:00</updated><id>https://tonym128.github.io/feed.xml</id><title type="html">ttech | Technology Adventures</title><subtitle>I endeavour to show technology adventures in new, upcoming and unexplored things.  Being an avid fan of technology in my personal and professional life. I like to explore the latest and greatest tech, preferably in the Open Source space.  Also on the radar are gaming technology and most things to do with graphics and some hardware tinkering on the side.</subtitle><author><name>Tony Mamacos</name></author><entry><title type="html">From Arduboy to PlayStation: What It Takes to Build Games with AI on Every System</title><link href="https://tonym128.github.io/2026/10/29/from-arduboy-to-playstation-building-games-with-ai.html" rel="alternate" type="text/html" title="From Arduboy to PlayStation: What It Takes to Build Games with AI on Every System" /><published>2026-10-29T00:00:00+00:00</published><updated>2026-10-29T00:00:00+00:00</updated><id>https://tonym128.github.io/2026/10/29/from-arduboy-to-playstation-building-games-with-ai</id><content type="html" xml:base="https://tonym128.github.io/2026/10/29/from-arduboy-to-playstation-building-games-with-ai.html"><![CDATA[<h1 id="from-arduboy-to-playstation-what-it-takes-to-build-games-with-ai-on-every-system">From Arduboy to PlayStation: What It Takes to Build Games with AI on Every System</h1>

<p>A year ago, <a href="/2025/12/29/peakylight-post-mortem.html">Peakylight</a> was my first real attempt at building something from scratch with AI. Since then I’ve used AI to build games for a 128x64 monochrome handheld, a $5 microcontroller, the NES, the Mega Drive, the Nintendo 64, the original PlayStation, the browser and a VR headset.</p>

<p>This post isn’t a list of games. It’s what I learnt climbing that hardware ladder: what each system demands, where the development time actually went, how the build pipelines grew up along the way, and what changed in how I work with AI because of it.</p>

<p>I built them roughly in the order <strong>web → Arduboy → ESP32 → N64 → PS1 → Mega Drive → NES</strong>. Here they’re ordered by hardware, from the smallest machine up, because that’s the order the lessons make the most sense in.</p>

<h2 id="the-machines-at-a-glance">The Machines at a Glance</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left">System</th>
      <th style="text-align: left">CPU</th>
      <th style="text-align: left">RAM</th>
      <th style="text-align: left">Storage / Budget</th>
      <th style="text-align: left">Games</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Arduboy</td>
      <td style="text-align: left">ATmega32U4, 8-bit @ 16 MHz</td>
      <td style="text-align: left"><strong>2.5 KB</strong></td>
      <td style="text-align: left">~29 KB flash (FX: 16 MB flash chip)</td>
      <td style="text-align: left">DownDraft, Stick Fighter, Sticks of Rage, Ether Echoes</td>
    </tr>
    <tr>
      <td style="text-align: left">NES</td>
      <td style="text-align: left">6502 @ 1.79 MHz</td>
      <td style="text-align: left"><strong>2 KB</strong></td>
      <td style="text-align: left">32 KB PRG + 8 KB CHR (NROM)</td>
      <td style="text-align: left">Lantern Moth</td>
    </tr>
    <tr>
      <td style="text-align: left">ESP32 (TTGO T-Display)</td>
      <td style="text-align: left">Dual Xtensa LX6 @ 240 MHz</td>
      <td style="text-align: left">~320 KB SRAM</td>
      <td style="text-align: left">4 MB flash</td>
      <td style="text-align: left">Another World ESP32</td>
    </tr>
    <tr>
      <td style="text-align: left">Mega Drive</td>
      <td style="text-align: left">68000 @ 7.67 MHz</td>
      <td style="text-align: left"><strong>64 KB</strong></td>
      <td style="text-align: left">64 KB VRAM, 4 palettes x 16 colours</td>
      <td style="text-align: left">Bridget <em>(coming soon)</em></td>
    </tr>
    <tr>
      <td style="text-align: left">Nintendo 64</td>
      <td style="text-align: left">VR4300 @ 93.75 MHz</td>
      <td style="text-align: left">4 MB</td>
      <td style="text-align: left">2 MB cartridge (in my build)</td>
      <td style="text-align: left">Another World N64</td>
    </tr>
    <tr>
      <td style="text-align: left">PlayStation 1</td>
      <td style="text-align: left">R3000A @ 33.87 MHz</td>
      <td style="text-align: left"><strong>2 MB</strong></td>
      <td style="text-align: left">1 MB VRAM, 512 KB SPU RAM, CD-ROM</td>
      <td style="text-align: left">Plattypus, Arduracer PSX</td>
    </tr>
    <tr>
      <td style="text-align: left">Web / WebXR</td>
      <td style="text-align: left">Whatever you’ve got</td>
      <td style="text-align: left">Lots</td>
      <td style="text-align: left">Frame time on a phone or headset</td>
      <td style="text-align: left">Aussie Meme Boss Rush (+VR), Sun Runner, Axis Rush</td>
    </tr>
  </tbody>
</table>

<p>Every row taught me something different. The odd thing is that the hardest machines weren’t always the smallest ones.</p>

<hr />

<h2 id="1-arduboy-25-kb-of-ram-and-a-lot-of-lines">1. Arduboy: 2.5 KB of RAM and a Lot of Lines</h2>

<p><img src="/images/2026/10/30/downdraft-gameplay.gif" alt="DownDraft gameplay" /></p>

<p>The Arduboy is where I learnt that AI writes code for the machine it <em>imagines</em>, not the one you have. With 2.5 KB of RAM and a 1-bit 128x64 screen, a single careless array is a crash, and <code class="language-plaintext highlighter-rouge">float</code> is a luxury you can’t afford.</p>

<p><strong>What I built:</strong></p>
<ul>
  <li><strong>DownDraft:</strong> a 1-bit vertical descent platformer with energy-based jumps, a smash-through “plummet” and EEPROM high scores.</li>
  <li><strong><a href="/2026/04/23/stick-fighter-arduboy.html">Stick Fighter</a>:</strong> a 10-character fighter using <strong>skeletal (bone) animation</strong> instead of sprite frames. That’s the only way that many fighters fit.</li>
  <li><strong><a href="/2026/04/24/sticks-of-rage-arduboy.html">Sticks of Rage</a>:</strong> the same “Stick Engine” grown into a side-scrolling beat ‘em up with parallax and swarming enemies.</li>
  <li><strong>Ether Echoes:</strong> an 8-character, 4-shade grayscale fighter for the Arduboy FX, written in the <a href="https://github.com/tiberiusbrown/abc">ABC language</a>.</li>
</ul>

<p><img src="/images/2026/05/10/gameplay-opt.jpg" alt="Sticks of Rage" /></p>

<p><img src="/images/2026/10/30/ether-echoes-title.png" alt="Ether Echoes title and Kael's stage art" />
<img src="/images/2026/10/30/ether-echoes-stage.png" alt="Ether Echoes stage background" />
<em>Ether Echoes: the title card and Kael’s 4-shade stage background.</em></p>

<h3 id="the-pipelines">The Pipelines</h3>

<p>I tried three completely different pipelines on the same hardware:</p>

<ol>
  <li>
    <p><strong>Browser IDE.</strong> DownDraft was built in Arduboy Cloud, with code, simulator and build output side by side. The build output is the most honest feedback loop on the platform: it tells you exactly how much flash and RAM you have left after every change.</p>

    <p><img src="/images/2026/10/30/downdraft-arduboy-cloud-opt.jpg" alt="DownDraft in Arduboy Cloud" />
<em>18,418 of 29,696 bytes of flash and 1,479 of 2,560 bytes of RAM. These numbers matter more than anything the AI says.</em></p>
  </li>
  <li><strong>Desktop twin.</strong> Stick Fighter and Sticks of Rage split into <code class="language-plaintext highlighter-rouge">Engine</code>, <code class="language-plaintext highlighter-rouge">Game</code> and <code class="language-plaintext highlighter-rouge">Platform</code> layers, so the same code builds for the Arduboy (<code class="language-plaintext highlighter-rouge">arduino-cli</code>) and for desktop SDL2. I also had the AI build a <strong>desktop skeletal animation editor</strong> to create and export poses.</li>
  <li><strong>A niche language.</strong> Ether Echoes is written in ABC and packaged as <code class="language-plaintext highlighter-rouge">main.abc</code> plus a media folder. AI has seen very little ABC, so I was much more involved in that one.</li>
</ol>

<h3 id="where-the-time-went">Where the Time Went</h3>

<p>Not in the gameplay code. It went into <strong>memory</strong> and <strong>tooling</strong>. The skeletal animation system and its editor took far longer than any fighter’s moveset. The SDL desktop build was a huge speed-up, but it also lied to me. On desktop, everything runs at a perfect 60 FPS with endless memory, so it’s easy to blow past the 32 KB flash limit without noticing.</p>

<p><strong>Lesson:</strong> a desktop twin is worth it, but <strong>flash the real hardware often</strong>. The AI only respects a constraint it can see failing.</p>

<hr />

<h2 id="2-esp32-porting-a-classic-onto-a-5-board">2. ESP32: Porting a Classic onto a $5 Board</h2>

<p><img src="/images/2026/08/another_world_esp32_demo.gif" alt="Another World on ESP32" /></p>

<p><a href="/2026/08/26/porting-another-world-to-esp32-50fps-vector-graphics-capacitive-touch.html">Another World on the ESP32</a> was my first port rather than an original game. It runs Éric Chahi’s bytecode VM at 50 FPS on a TTGO T-Display, with a custom capacitive touch gamepad PCB and Bluetooth audio.</p>

<p><strong>Pipeline:</strong> PlatformIO, LovyanGFX for DMA display transfers, and a <code class="language-plaintext highlighter-rouge">pack_assets.py --demo</code> script that downloads the shareware demo and flashes it to SPIFFS. That keeps the repo free of copyrighted data. This was the first project where I wrote a proper <code class="language-plaintext highlighter-rouge">AGENTS.md</code> describing the architecture for the AI to follow.</p>

<h3 id="where-the-time-went-1">Where the Time Went</h3>

<ul>
  <li><strong>RAM.</strong> The original engine wants four 32,000-byte framebuffers (128 KB). With only ~160 KB of free heap, the game’s 65 KB cinematic polygon table failed to allocate, and Lester stood on a black screen. The fix was aliasing down to two physical buffers.</li>
  <li><strong>Hardware gotchas no model would guess.</strong> <code class="language-plaintext highlighter-rouge">GPIO 12</code> is an ESP32 strapping pin. Touching that pad at power-on put the board into a boot loop, so the controls had to be re-routed.</li>
</ul>

<p><strong>Lesson:</strong> on real hardware, half the bugs live outside the code. The AI is great at the VM, but someone has to hold the board.</p>

<hr />

<h2 id="3-nes-2-kb-of-ram-and-27000-cycles-a-frame">3. NES: 2 KB of RAM and 27,000 Cycles a Frame</h2>

<p><img src="/images/2026/10/30/lantern-moth-title.gif" alt="Lantern Moth title" /></p>

<p><a href="https://tonym128.github.io/lantern-moth/">Lantern Moth</a> is my newest game, and the closest to the metal: <strong>hand-written 6502 assembly</strong> on a plain NROM cartridge with no extra chips. You’re a moth flying loops of light around cave creatures to cocoon them, with 15 stages, 3 bosses and 2-player co-op.</p>

<p><img src="/images/2026/10/30/lantern-moth-coop.gif" alt="Lantern Moth co-op" /></p>

<h3 id="the-constraints">The Constraints</h3>

<p>Every frame has to fit in about <strong>27,000 CPU cycles</strong>. The finished game has <strong>136 bytes</strong> of general RAM free and 22 bytes of zero page. The core mechanic, detecting when a trail closes a loop and what’s inside it, is a polygon-fill problem. On a 6502 that means <strong>scanline parity with precomputed XOR tables</strong>: no flood fill, no stack, and the work spread across several frames so no single frame overruns.</p>

<p>The lighting is all palettes and attribute tables, so it costs no tiles. Moths and cocoons store only their left half and are mirrored. Stage maps are packed at 4 bits a cell.</p>

<h3 id="the-pipeline">The Pipeline</h3>

<p>This one was the most complete pipeline I’ve had:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">make</code> runs Python asset generators, then <code class="language-plaintext highlighter-rouge">ca65</code>/<code class="language-plaintext highlighter-rouge">ld65</code>, then <code class="language-plaintext highlighter-rouge">usage.py</code>, which prints PRG, RAM and zero-page usage and <strong>fails the build</strong> if any of them runs short.</li>
  <li><code class="language-plaintext highlighter-rouge">make test</code> runs the ROM <strong>headless in jsnes</strong> through 134 checks, from clearing all 15 stages by honest loops to co-op revives and zero lag frames.</li>
  <li><code class="language-plaintext highlighter-rouge">make stress</code> flies two moths hard round every stage and reports the worst frame. The worst stress frame came in around 23,900 cycles.</li>
  <li>Mesen with source-level debug symbols for anything the tests can’t see.</li>
  <li>A full <strong>release kit</strong>: manual, box art, cartridge label and trailer.</li>
</ul>

<p><img src="/images/2026/10/30/lantern-moth-box-opt.jpg" alt="Lantern Moth box art" /></p>

<h3 id="where-the-time-went-2">Where the Time Went</h3>

<p>The <strong>frame budget</strong>. Getting two moths, crossing trails, team loops and the light rebuild to fit inside one frame took most of the effort, by staggering work across frames, skipping work on busy frames and turning searches into bit tests.</p>

<p><strong>Lesson:</strong> the README ends with a <strong>“Not verified”</strong> section. Sound was never actually listened to, the PAL path is untested, and boss pacing was tuned by scripts rather than by hand. I now ask for that section on every project. Knowing what <em>wasn’t</em> checked is as valuable as the test suite.</p>

<hr />

<h2 id="4-mega-drive-bridget-coming-soon">4. Mega Drive: Bridget (Coming Soon)</h2>

<p><img src="/images/2026/10/30/bridget-title.png" alt="Bridget title screen" /></p>

<p><strong>Bridget</strong> is a bridge-building physics puzzle for the Sega Mega Drive / Genesis, written in C with SGDK. You build from wood, steel and cable on a budget, then drive vehicles across and watch the Verlet physics decide whether you’re an engineer. It isn’t public yet, but it’s coming soon.</p>

<p><img src="/images/2026/10/30/bridget-attract-demo.png" alt="Bridget attract demo" />
<em>The attract-mode demo running in Mednafen.</em></p>

<h3 id="the-constraints-1">The Constraints</h3>

<p>The 68000 has <strong>no floating point</strong>, so the physics core is pure <strong>Q16.16 fixed point</strong>. It has to produce exactly the same results on NTSC 60 Hz and PAL 50 Hz consoles, or a bridge that holds on one machine collapses on another. There’s no <code class="language-plaintext highlighter-rouge">malloc</code> anywhere. Everything is statically allocated, and the README puts the resident footprint under 18 KB of the 64 KB work RAM.</p>

<h3 id="the-pipeline-1">The Pipeline</h3>

<p>Bridget was the most <em>planned</em> project. It started with a <code class="language-plaintext highlighter-rouge">GAMEPLAN.md</code>, an <code class="language-plaintext highlighter-rouge">AGENT.md</code> and a <code class="language-plaintext highlighter-rouge">TODOLIST.md</code> of 10 milestones and 100 steps, and the AI worked through them one milestone at a time:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/core</code> is portable C with no SGDK includes, so the physics is tested natively with <code class="language-plaintext highlighter-rouge">gcc</code> and again under <strong>ASan + UBSan</strong>.</li>
  <li>The ROM builds with a vendored m68k toolchain, and CI produces a ROM <strong>byte-identical</strong> to the local build.</li>
  <li><code class="language-plaintext highlighter-rouge">emeshot.py</code> drives Mednafen headlessly, presses buttons and screenshots each screen, so every milestone has visual proof. The screenshots above came from it.</li>
</ul>

<h3 id="where-the-time-went-3">Where the Time Went</h3>

<p>Into the physics and determinism. Making a structure behave the same way on both refresh rates is a bigger problem than drawing it.</p>

<p><strong>Lesson:</strong> on a fixed-point machine, design the maths first. Splitting the core from the hardware layer is what made it testable at all.</p>

<hr />

<h2 id="5-nintendo-64-the-one-day-port">5. Nintendo 64: The One-Day Port</h2>

<p><img src="/images/2026/10/30/another-world-n64-opt.jpg" alt="Another World on N64" /></p>

<p>Once the Another World VM worked on the ESP32, the obvious question was “where else can it run?” The N64 port is built with <strong>LibDragon</strong> inside Docker, packs the game data with DragonFS into the ROM, and a <code class="language-plaintext highlighter-rouge">build_rom.sh</code> script pads the binary and signs the <strong>CIC-6102 boot checksum</strong>. Host-side unit tests check the checksum algorithm and the ROM header.</p>

<h3 id="where-the-time-went-4">Where the Time Went</h3>

<p>Almost nowhere, and that’s the point. All the commits for this one landed in a single day, because the hard work (the VM, the renderer, the audio mixer) already existed and only the platform layer was new.</p>

<p><strong>Lesson:</strong> a clean platform layer pays off twice. The second port is nearly free.</p>

<hr />

<h2 id="6-playstation-1-bare-metal-rust-and-the-biggest-lessons">6. PlayStation 1: Bare-Metal Rust and the Biggest Lessons</h2>

<p>Both PS1 games are written in <code class="language-plaintext highlighter-rouge">#![no_std]</code> Rust and compile to MIPS R3000A machine code with the <a href="https://github.com/EBonura/PSoXide">PSoXide</a> SDK. That means 2 MB of RAM, 1 MB of VRAM, no operating system, and the GTE, MDEC and SPU coprocessors to drive by hand. These two projects were the longest by far: Plattypus has 140 commits over 14 days, and Arduracer PSX has 361 commits over 17 days.</p>

<h3 id="plattypus">Plattypus</h3>

<p><img src="/images/2026/10/30/plattypus-frontscreen-opt.jpg" alt="Plattypus" /></p>

<p><a href="https://tonym128.github.io/plattypus/">Plattypus</a> is a 3D tactical stealth game about a platypus breaking out of Healesville Sanctuary to get home to his new baby sister. It started on day one as a 2D platformer and was re-imagined as 3D stealth action within hours, which is the kind of pivot AI makes cheap. It has full-motion video cutscenes on the MDEC, a Red Book CD audio soundtrack, and a web player built on my <a href="https://github.com/tonym128/PSoXide-emulator">fork of the PSoXide emulator</a>.</p>

<p><strong>Where the time went: the intro video.</strong> The FMV played at 6 FPS. The investigation measured each frame at <strong>110 ms of work for a 66.67 ms budget</strong> (661%), and it records <strong>two diagnoses that turned out to be wrong</strong>. One was “the MDEC is backed up”, which was a misread status register. Another was “the decoder can’t run while the CD is reading”, which a six-way probe disproved. Three fixes were built and measured, and none of them reached 15 FPS. What finally worked was changing the <strong>codec</strong> so each frame needed 5 CD sectors instead of 7. That got it to 148 of 148 frames at 15 FPS.</p>

<h3 id="arduracer-psx">Arduracer PSX</h3>

<p><img src="/images/2026/10/30/arduracer-psx-race.png" alt="Arduracer PSX" /></p>

<p><a href="https://tonym128.github.io/ArduracerPSX/">Arduracer PSX</a> takes my 8-bit ArduRacer FX time-trial racer to 32-bit colour, with 24 circuits, ghosts, AI rivals, car tuning, DualShock rumble and memory card saves.</p>

<p>The <strong>pipeline</strong> went fully multi-agent. An <code class="language-plaintext highlighter-rouge">AGENTS.md</code> sets the rules: every change in its own git worktree, every branch reviewed from four perspectives (architect, systems engineer, game designer, QA) before merging, <code class="language-plaintext highlighter-rouge">clippy -D warnings</code>, no panics in game code, and a <code class="language-plaintext highlighter-rouge">tools/playtest</code> reference driver that actually drives every circuit. <code class="language-plaintext highlighter-rouge">make disc</code> masters a BIN/CUE with CD audio tracks.</p>

<p><strong>Where the time went: finding out what wasn’t real.</strong> The audits in that repo were the most humbling part of the year:</p>

<ul>
  <li>At one point <strong>no lap could ever be completed</strong>, cars moved ~30x too slowly, and the memory card never saved. Every write panicked.</li>
  <li><code class="language-plaintext highlighter-rouge">Fixed::mul</code> silently wrapped on overflow, so any distance over ~800 units came back as <code class="language-plaintext highlighter-rouge">0.00</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">cargo test</code> on the core crate <strong>ran zero tests and exited 0</strong>. The CI was green while testing nothing.</li>
  <li>Whole features (force feedback, curb rumble, semi-transparency) were described as done but had no callers.</li>
</ul>

<p>Plattypus had a similar finding: a test harness that <strong>mirrored</strong> the game logic instead of testing it. That’s why “tests must exercise the real core crate” is now rule #3 in my <code class="language-plaintext highlighter-rouge">AGENTS.md</code>.</p>

<p><strong>Lesson:</strong> green isn’t proof. AI is very good at producing things that <em>look</em> finished, like passing CI, sign-off tables and feature lists. On the PS1, the only thing that counted was a measurement or a reference driver actually finishing a lap.</p>

<hr />

<h2 id="7-web-and-vr-where-it-all-started">7. Web and VR: Where It All Started</h2>

<p><img src="/images/2026/02/23/aussie-meme-rush-opt.jpg" alt="Aussie Meme Boss Rush" /></p>

<p>The browser was the easy end of the ladder, and the right place to start:</p>

<ul>
  <li><strong><a href="/2026/02/22/aussie-meme-boss-rush.html">Aussie Meme Boss Rush</a>:</strong> a neon Three.js boss rush against cybernetic Australian wildlife memes.</li>
  <li><strong><a href="/2026/03/02/sunrunner-high-speed-polygonal-rail-shooter.html">Sun Runner</a>:</strong> a low-poly rail shooter diving into the sun.</li>
  <li><strong><a href="https://tonym128.github.io/axis_rush/">Axis Rush</a>:</strong> an F-Zero-style racer on a cylindrical track you can flip inside and out of, textured at runtime by my <a href="/2026/03/04/texgen-procedural-texture-generator.html">TexGen</a> shader tool. <a href="/2026/03/17/axis-rush-high-speed-cylindrical-racing.html">Write-up</a>.</li>
</ul>

<p><img src="/images/2026/02/23/sunrunner.jpg" alt="Sun Runner" /></p>

<p><img src="/images/2026/10/30/axis-rush-opt.jpg" alt="Axis Rush" /></p>

<p><strong>Pipeline:</strong> Gemini CLI, a <code class="language-plaintext highlighter-rouge">GEMINI.md</code> of project rules, and a <strong>single <code class="language-plaintext highlighter-rouge">index.html</code></strong> with procedural shaders and Web Audio instead of asset files. One file meant the AI always had the whole game in context, and “deploy” meant pushing to GitHub Pages.</p>

<h3 id="going-vr">Going VR</h3>

<p>Four days after Aussie Meme Boss Rush shipped, it got a <strong>WebXR version</strong>. It turned out to be far more than a toggle. The desktop game used <strong>2D screen-space hit detection</strong>, projecting everything onto the screen and checking where you clicked. In VR there is no screen to click, so targeting had to be rebuilt around <strong>controller raycasts</strong> from a blaster in each hand. It also gained haptic feedback on each shot and menus rendered onto in-world canvas textures.</p>

<p><strong>Where the time went:</strong> debugging. You can’t open devtools inside a headset, so the VR build has its own <strong>on-screen debug console</strong> and <strong>simulated controllers</strong> for testing on desktop. Most of the VR commits are some version of “VR fixes”.</p>

<p><strong>Lesson:</strong> a new input model means a new game. The rendering ported in an afternoon; the interaction didn’t.</p>

<hr />

<h2 id="what-i-learnt-across-every-system">What I Learnt Across Every System</h2>

<p><strong>1. Put the budget in the build.</strong> The best projects made the constraint impossible to ignore: Arduboy Cloud’s flash and RAM bars, Lantern Moth’s <code class="language-plaintext highlighter-rouge">usage.py</code> failing the build, Arduracer’s RAM budget check in CI. An AI respects a number that breaks the build. It doesn’t respect a sentence in a prompt.</p>

<p><strong>2. Measure, don’t guess.</strong> AI explanations of performance problems sound authoritative and are often wrong. Plattypus’s FMV investigation went through two confident wrong diagnoses before measurements found the real bottleneck.</p>

<p><strong>3. Green isn’t proof.</strong> Tests that run nothing, harnesses that mirror the code, features with no callers. Every bigger project needed a fresh-eyes audit that verified claims by running things.</p>

<p><strong>4. Give the AI eyes.</strong> jsnes running Lantern Moth headless, <code class="language-plaintext highlighter-rouge">emeshot.py</code> screenshotting Bridget in Mednafen, Arduracer’s playtest driver. The projects improved fastest once the AI could see and play what it had built.</p>

<p><strong>5. Separate the core from the hardware.</strong> The same pattern appeared everywhere: SDL plus Arduboy, a pure Rust <code class="language-plaintext highlighter-rouge">core</code> crate, Bridget’s <code class="language-plaintext highlighter-rouge">src/core</code>, and the Another World VM moving from ESP32 to N64. It makes code testable on a PC, and makes the next port cheap.</p>

<p><strong>6. Write the rules down, and grow them.</strong> My agent files went from a few lines of style notes in <code class="language-plaintext highlighter-rouge">GEMINI.md</code> to <code class="language-plaintext highlighter-rouge">AGENTS.md</code> protocols with worktrees, multi-perspective reviews and banned patterns. Most of the improvement in the later projects came from those files, not from better prompts.</p>

<p><strong>7. Ask for what wasn’t verified.</strong> Lantern Moth’s “Not verified” section is now my favourite part of any README.</p>

<p><strong>8. The fun is still my job.</strong> Boss pacing tuned by scripts, combos that feel different on a keyboard than on real buttons, a VR game that needed new hands. The AI writes most of the code, but deciding whether it feels right means playing it on the real thing.</p>

<hr />

<h2 id="turning-the-lessons-into-an-agents-file">Turning the Lessons into an Agents File</h2>

<p>Lessons are only useful if the AI actually follows them, and the place for that is the agents file it reads before every session. Here’s the <code class="language-plaintext highlighter-rouge">AGENTS.md</code> template I’ve distilled from all of the above. Every rule in it exists because one of these projects needed it.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># AGENTS.md</span>

Read this before changing anything. GAMEPLAN.md says what we're building, TODO.md says
where we are, and this file says how to work.

<span class="gu">## 1. The target machine</span>
| Resource | Limit | Current use | Where it's checked |
|---|---|---|---|
| CPU / frame | e.g. ~27,000 cycles (NES NTSC) | | <span class="sb">`make stress`</span> |
| RAM | e.g. 2.5 KB / 2 KB / 64 KB / 2 MB | | <span class="sb">`make size`</span> |
| ROM / flash | | | <span class="sb">`make size`</span> |
<span class="p">
-</span> Write code for this machine, not for a PC. If a technique assumes floats, a heap,
  an OS or plenty of memory, it's wrong here until proven otherwise.
<span class="p">-</span> Use fixed point and say the format (e.g. Q16.16). Multiplies must saturate:
  no silent wraparound.
<span class="p">-</span> No dynamic allocation on constrained targets.
<span class="p">-</span> If a change moves any number in this table, update the table in the same commit.

<span class="gu">## 2. Budgets live in the build, not in prose</span>
<span class="p">-</span> The build prints resource usage after every compile and FAILS if a budget is exceeded.
<span class="p">-</span> A stress test reports the worst frame. A new worst frame is a regression.
<span class="p">-</span> Never "fix" a budget failure by raising the limit without a written reason.

<span class="gu">## 3. Core and platform are separate</span>
<span class="p">-</span> core/ is portable game logic and maths that builds and tests on the host.
<span class="p">-</span> platform/ is display, input, audio and SDK glue. Keep it thin.
<span class="p">-</span> Desktop builds are for fast iteration only. Before calling a feature done,
  check it on real hardware or an accurate emulator.

<span class="gu">## 4. Testing</span>
<span class="p">-</span> Tests exercise the REAL core code, never a copy or mirror of it.
<span class="p">-</span> A test command that runs zero tests is a failure. Check the count, not the exit code.
<span class="p">-</span> Give yourself eyes: headless emulator runs, scripted screenshots, a bot that plays.
<span class="p">-</span> Same inputs give the same result on NTSC and PAL. CI builds match local builds.

<span class="gu">## 5. Evidence over assertion</span>
<span class="p">-</span> Measure, don't guess. A performance claim needs a measurement and the command
  that produced it. Otherwise label it a hypothesis.
<span class="p">-</span> When a diagnosis turns out wrong, record that. Don't quietly rewrite history.
<span class="p">-</span> Never write a report, sign-off, playtest or hardware result that did not happen.
<span class="p">-</span> A feature is done only when it has a caller, is reachable, and was seen working.

<span class="gu">## 6. Always include a "Not verified" section</span>
Every README, PR and milestone summary ends with what was NOT tested and why:
no hardware, no audio output, emulator lacks PAL, tuned by script not by hand.

<span class="gu">## 7. Workflow</span>
<span class="p">-</span> One branch or git worktree per change. Never commit directly to main.
<span class="p">-</span> Review from four perspectives before merging: architect, systems/hardware,
  game designer, QA.
<span class="p">-</span> Format, lint (warnings as errors) and test before every commit.
<span class="p">-</span> No panics on shipping code paths. A save that fails must say so.

<span class="gu">## 8. Hardware gotchas</span>
<span class="p">-</span> One line per bite, e.g. "ESP32 GPIO 12 is a strapping pin: high at boot means a boot loop."

<span class="gu">## 9. The human owns</span>
<span class="p">-</span> Game feel, difficulty and pacing sign-off.
<span class="p">-</span> Real hardware testing, and any claim about it.
<span class="p">-</span> Scope changes and cuts. Propose them; don't make them silently.
</code></pre></div></div>

<p>Where the rules came from:</p>

<ul>
  <li><strong>Budgets in the build:</strong> Arduboy Cloud’s flash and RAM bars, and Lantern Moth’s <code class="language-plaintext highlighter-rouge">usage.py</code> failing the build.</li>
  <li><strong>Core and platform separate:</strong> the SDL desktop twin of Stick Fighter, and the Another World VM moving from ESP32 to N64.</li>
  <li><strong>Tests exercise the real code:</strong> Plattypus’s harness that mirrored the game, and Arduracer’s <code class="language-plaintext highlighter-rouge">cargo test</code> running zero tests.</li>
  <li><strong>Saturating maths:</strong> Arduracer’s <code class="language-plaintext highlighter-rouge">Fixed::mul</code> wraparound, which turned long distances into <code class="language-plaintext highlighter-rouge">0.00</code>.</li>
  <li><strong>Measure, don’t guess:</strong> Plattypus’s two wrong FMV diagnoses.</li>
  <li><strong>Never write a result that didn’t happen:</strong> sign-off tables and feature lists that looked finished but weren’t.</li>
  <li><strong>“Not verified”:</strong> borrowed straight from Lantern Moth’s README.</li>
  <li><strong>Four-perspective review and worktrees:</strong> the Arduracer PSX multi-agent setup.</li>
  <li><strong>Hardware gotchas:</strong> the ESP32 strapping pin that no model would have guessed.</li>
  <li><strong>The human owns the fun:</strong> every game on this list.</li>
</ul>

<p>A year ago I wouldn’t have believed I’d ship a PlayStation game in bare-metal Rust or an NES game in 6502 assembly. Bridget is next out of the oven. After that, who knows. Maybe the Saturn?</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[A journey through building games with AI on the Arduboy, ESP32, NES, Mega Drive, N64, PlayStation 1, the web and VR. What each machine's constraints taught me, where the development time really went, and the pipelines that made it work.]]></summary></entry><entry><title type="html">Porting Another World to ESP32: 50 FPS Vector Graphics, Capacitive Touch, and Bluetooth Audio on a $5 Microcontroller</title><link href="https://tonym128.github.io/2026/08/26/porting-another-world-to-esp32-50fps-vector-graphics-capacitive-touch.html" rel="alternate" type="text/html" title="Porting Another World to ESP32: 50 FPS Vector Graphics, Capacitive Touch, and Bluetooth Audio on a $5 Microcontroller" /><published>2026-08-26T10:00:00+00:00</published><updated>2026-08-26T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/08/26/porting-another-world-to-esp32-50fps-vector-graphics-capacitive-touch</id><content type="html" xml:base="https://tonym128.github.io/2026/08/26/porting-another-world-to-esp32-50fps-vector-graphics-capacitive-touch.html"><![CDATA[<h1 id="porting-another-world-to-esp32-50-fps-vector-graphics-capacitive-touch-and-bluetooth-audio-on-a-5-microcontroller">Porting Another World to ESP32: 50 FPS Vector Graphics, Capacitive Touch, and Bluetooth Audio on a $5 Microcontroller</h1>

<p>In 1991, French visionary Éric Chahi released <strong>Another World</strong> (<em>Out of This World</em> in North America)—a cinematic tour de force that revolutionized video game storytelling. Built entirely on an avant-garde vector polygon engine and a custom 64-thread virtual machine, the game squeezed smooth cinematic cutscenes onto 7.14 MHz Amiga 500 computers with just 512 KB of RAM.</p>

<p>For a long time, I wondered: <strong>Could we run the complete Another World engine natively on a bare-metal ESP32 microcontroller with fluid 50 FPS rendering, capacitive touch controls on a custom PCB, and Bluetooth audio streaming?</strong></p>

<p>Here is the story of how <strong><code class="language-plaintext highlighter-rouge">anotheresp32</code></strong> was engineered, the memory and hardware obstacles overcome, and how you can run it on your own board.</p>

<p><img src="/images/2026/08/another_world_esp32_demo.gif" alt="Another World ESP32 Running with Custom Capacitive Touch Gamepad" /></p>

<hr />

<h2 id="the-hardware-lilygo-ttgo-t-display--ttech-game-pad">The Hardware: LilyGO TTGO T-Display &amp; “TTECH Game Pad”</h2>

<p>The project targets the popular <strong>LilyGO TTGO T-Display</strong>:</p>
<ul>
  <li><strong>MCU</strong>: Espressif ESP32-D0WDQ6 (Dual-Core Xtensa LX6 @ 240 MHz, ~320 KB SRAM, 4 MB Flash)</li>
  <li><strong>Display</strong>: 1.14” ST7789 IPS TFT (240x135 pixels, 16-bit RGB565 over high-speed SPI)</li>
  <li><strong>Cost</strong>: ~$5–$8</li>
</ul>

<p>To make it a true standalone portable console, I etched and milled a custom PCB gamepad: the <strong>TTECH ESP32 Game Pad</strong>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                        +-------------------+
                        |    [USB-C PORT]   |
                        |                   |
                        |   TTGO T-Display  |
                        |   [240x135 IPS]   |
                        +-------------------+
                              |   |   |
                  +-----------+---+---+-----------+
                  |  TTECH ESP32 Capacitive Pad   |
                  |                               |
                  |       (UP: GPIO 15)           |
                  |             |                 |
                  | (LEFT: 2) --+-- (RIGHT: 13)   |  (ACTION: 27)
                  |             |                 |
                  |      (DOWN: GPIO 32)          |  (JUMP: 33)
                  |                               |
                  |       [START: GPIO 12]        |
                  +-------------------------------+
</code></pre></div></div>

<p>Instead of using clicky tactile buttons, the board features <strong>7 bare-copper capacitive touch pads</strong> connected directly to the ESP32’s internal touch sensor peripherals (<code class="language-plaintext highlighter-rouge">Touch 2</code> through <code class="language-plaintext highlighter-rouge">Touch 9</code>).</p>

<hr />

<h2 id="technical-obstacle-1-the-boot-strapping-pin-trap">Technical Obstacle 1: The Boot-Strapping Pin Trap</h2>

<p>During PCB design, <code class="language-plaintext highlighter-rouge">GPIO 12</code> was originally routed to <strong>DOWN</strong>. However, <code class="language-plaintext highlighter-rouge">GPIO 12</code> is MTDI—a critical ESP32 strapping pin that sets the flash voltage regulator to 1.8V if pulled high at power-on! If a player touches the pad or trace resistance pulls <code class="language-plaintext highlighter-rouge">GPIO 12</code> high during boot, the flash voltage drops and the chip enters an unrecoverable boot loop (<code class="language-plaintext highlighter-rouge">flash read err, 1000</code>).</p>

<p><strong>The Solution</strong>:
We remapped the default movement controls strictly to safe, non-strapping pins:</p>
<ul>
  <li><strong>LEFT</strong>: <code class="language-plaintext highlighter-rouge">GPIO 2</code> (<code class="language-plaintext highlighter-rouge">Touch 2</code>)</li>
  <li><strong>UP</strong>: <code class="language-plaintext highlighter-rouge">GPIO 15</code> (<code class="language-plaintext highlighter-rouge">Touch 3</code>)</li>
  <li><strong>RIGHT</strong>: <code class="language-plaintext highlighter-rouge">GPIO 13</code> (<code class="language-plaintext highlighter-rouge">Touch 4</code>)</li>
  <li><strong>DOWN</strong>: <code class="language-plaintext highlighter-rouge">GPIO 32</code> (<code class="language-plaintext highlighter-rouge">Touch 9</code>) — <em>Safe!</em></li>
  <li><strong>ACTION</strong>: <code class="language-plaintext highlighter-rouge">GPIO 27</code> (<code class="language-plaintext highlighter-rouge">Touch 7</code>)</li>
  <li><strong>TOUCH JUMP</strong>: <code class="language-plaintext highlighter-rouge">GPIO 33</code> (<code class="language-plaintext highlighter-rouge">Touch 8</code>)</li>
  <li><strong>START / PAUSE (Optional)</strong>: <code class="language-plaintext highlighter-rouge">GPIO 12</code> (<code class="language-plaintext highlighter-rouge">Touch 5</code>)</li>
</ul>

<p>At boot, the driver executes a 100ms baseline calibration cycle on all 7 channels, computing a resting threshold for instantaneous, jitter-free touch response.</p>

<hr />

<h2 id="technical-obstacle-2-the-160-kb-sram-puzzle--65-kb-polygon-tables">Technical Obstacle 2: The 160 KB SRAM Puzzle &amp; 65 KB Polygon Tables</h2>

<p>The native Another World engine renders at <strong>320x200 (4-bit indexed color, 16 colors)</strong>. Each frame page occupies <strong>32,000 bytes</strong>.</p>

<p>The original VM engine relies on <strong>4 visual pages</strong>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Page 0</code>: Display page (front buffer)</li>
  <li><code class="language-plaintext highlighter-rouge">Page 1</code>: Back buffer (drawing canvas)</li>
  <li><code class="language-plaintext highlighter-rouge">Page 2</code>: Background persistence buffer</li>
  <li><code class="language-plaintext highlighter-rouge">Page 3</code>: Composition / sprite layer</li>
</ul>

<h3 id="the-problem">The Problem:</h3>
<p><code class="language-plaintext highlighter-rouge">4 x 32,000 bytes = 128,000 bytes (128 KB)</code>. 
On an ESP32 with ~160 KB of free application heap, reserving 128 KB upfront left only ~32 KB of remaining heap. When Part 16001 loaded its <strong>65,156-byte cinematic polygon table (<code class="language-plaintext highlighter-rouge">res 25</code>)</strong>, <code class="language-plaintext highlighter-rouge">malloc()</code> failed, leaving Lester standing on a pitch-black screen.</p>

<h3 id="the-engineering-fix">The Engineering Fix:</h3>
<p>By analyzing the VM opcode stream, we found that Another World rarely needs 4 distinct physical buffers simultaneously; pages 2 and 3 are largely composition scratchpads.
We re-architected the video subsystem to allocate <strong>2 high-speed physical framebuffers (64 KB total)</strong> with page aliasing.</p>

<p>This dropped baseline video SRAM consumption from <strong>128 KB to 64 KB</strong>, freeing up <strong>~160 KB of contiguous heap</strong>. Now, massive 65 KB polygon tables, sound banks, and Amiga audio modules allocate instantly without needing external PSRAM!</p>

<hr />

<h2 id="technical-obstacle-3-50-fps-vector-polygon-rasterization">Technical Obstacle 3: 50 FPS Vector Polygon Rasterization</h2>

<p>Another World does not draw bitmap sprites—it renders dynamic 2D vector polygons grouped in hierarchical transformation trees.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       [Raw Shape Bytecode]
                 │
                 ▼
    [Vertex Transform &amp; Zoom]
                 │
                 ▼
     [Left &amp; Right Edge Walkers]
                 │
                 ▼
    [4-Bit Nibble Packed Scanlines]
                 │
                 ▼ (SPI DMA via LovyanGFX)
    [ST7789 240x135 IPS TFT @ 50 Hz]
</code></pre></div></div>

<p>To achieve 50 FPS on the ST7789:</p>
<ol>
  <li><strong>Integer Fixed-Point Edge Walking</strong>: All polygon bounding box calculations and slope interpolations use fixed-point arithmetic.</li>
  <li><strong>Scanline Nibble Packing</strong>: The 4-bit indexed page buffers pack two pixels per byte, allowing fast horizontal word fills.</li>
  <li><strong>SPI DMA Push via LovyanGFX</strong>: Framebuffer updates are converted to 16-bit RGB565 and pushed across hardware SPI at 80 MHz using LovyanGFX’s DMA bus.</li>
</ol>

<p>The engine supports two scaling modes in <code class="language-plaintext highlighter-rouge">hw_config.h</code>:</p>
<ul>
  <li><strong>Letterbox Mode (Default)</strong>: Authentic 16:10 aspect ratio (216x135) centered with clean black pillarboxes.</li>
  <li><strong>Stretched Mode</strong>: Full 240x135 non-uniform screen stretch.</li>
</ul>

<hr />

<h2 id="technical-obstacle-4-audio-subsystem-8-bit-dac--bluetooth-a2dp">Technical Obstacle 4: Audio Subsystem (8-Bit DAC &amp; Bluetooth A2DP)</h2>

<p>Another World’s iconic audio was composed on Amiga 4-channel MODs and digitized 8-bit PCM samples (thunder cracks, elevator whirs, alien beast growls).</p>

<p>We implemented a dual audio subsystem:</p>
<ol>
  <li><strong>Zero-Overhead 8-bit DAC Output (Default)</strong>:
    <ul>
      <li>Channels mixed in real-time at 22,050 Hz and streamed via DMA to <code class="language-plaintext highlighter-rouge">GPIO 25</code> (DAC1) and <code class="language-plaintext highlighter-rouge">GPIO 26</code> (DAC2).</li>
    </ul>
  </li>
  <li><strong>Wireless Bluetooth A2DP Audio</strong>:
    <ul>
      <li>Mixed audio is upsampled to 44.1 kHz stereo and streamed to Bluetooth speakers or headphones.</li>
      <li>Includes direct hardware MAC auto-pairing in <code class="language-plaintext highlighter-rouge">include/bt_mac.h</code> for instant connection without scan delays.</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="technical-obstacle-5-commercial-asset-policy--1-command-demo-loader">Technical Obstacle 5: Commercial Asset Policy &amp; 1-Command Demo Loader</h2>

<p>A major priority was ensuring the repository remains <strong>100% legal, open-source, and free of DMCA risks</strong>. Commercial game banks (<code class="language-plaintext highlighter-rouge">bank01</code>–<code class="language-plaintext highlighter-rouge">bank0d</code>, <code class="language-plaintext highlighter-rouge">memlist.bin</code>) must never be checked into Git.</p>

<p>To give anyone an instant, out-of-the-box playable experience without hunting for floppy disks, we built the <strong><code class="language-plaintext highlighter-rouge">--demo</code></strong> auto-fetcher into <a href="https://github.com/tonym128/anotheresp32/blob/master/tools/pack_assets.py"><code class="language-plaintext highlighter-rouge">tools/pack_assets.py</code></a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Clone the clean C++ engine repo</span>
git clone https://github.com/tonym128/anotheresp32.git
<span class="nb">cd </span>anotheresp32

<span class="c"># 2. Automatically download the 1991 public shareware demo archive</span>
python3 tools/pack_assets.py <span class="nt">--demo</span>

<span class="c"># 3. Flash SPIFFS filesystem and firmware to ESP32!</span>
pio run <span class="nt">-t</span> uploadfs <span class="o">&amp;&amp;</span> pio run <span class="nt">-t</span> upload
</code></pre></div></div>

<p>The engine automatically detects whether you’re running the <strong>1991 Shareware Demo</strong> (smoothly looping after the wilderness chase) or the <strong>Full 13-Level Game</strong> (playing all the way to the fortress palace ending).</p>

<hr />

<h2 id="source-code--hardware-details">Source Code &amp; Hardware Details</h2>

<p>The entire codebase, circuit diagrams, test suite, and CI workflows are open-source under the MIT License on GitHub:</p>

<p>👉 <strong><a href="https://github.com/tonym128/anotheresp32">github.com/tonym128/anotheresp32</a></strong></p>

<h3 id="quick-requirements">Quick Requirements:</h3>
<ul>
  <li><strong>Board</strong>: LilyGO TTGO T-Display ESP32</li>
  <li><strong>Toolchain</strong>: PlatformIO (VS Code or CLI)</li>
  <li><strong>Assets</strong>: Run <code class="language-plaintext highlighter-rouge">python3 tools/pack_assets.py --demo</code> or pack your own DOS/Amiga files!</li>
</ul>

<hr />

<h2 id="acknowledgments--attributions">Acknowledgments &amp; Attributions</h2>

<p>This project was made possible thanks to the work and reverse-engineering of several pioneers in retro computing:</p>

<ul>
  <li><strong><a href="https://twitter.com/eric_chahi">Éric Chahi</a></strong>: Original visionary creator, designer, and programmer of <em>Another World</em> / <em>Out of This World</em> (1991, Delphine Software).</li>
  <li><strong><a href="https://fabiensanglard.net/">Fabien Sanglard</a></strong>: For his iconic technical book and articles on the engine:
    <ul>
      <li><a href="https://fabiensanglard.net/anotherWorld_code_review/"><em>Another World Engine Code Review &amp; Architecture Analysis</em></a></li>
      <li><a href="https://github.com/fabiensanglard/Another-World-Bytecode-Interpreter"><em>Another-World-Bytecode-Interpreter</em></a></li>
    </ul>
  </li>
  <li><strong><a href="https://github.com/cyxx">Gregory Montoir (cyxx)</a></strong>: Author of <strong><code class="language-plaintext highlighter-rouge">rawgl</code></strong> (Another World engine rewrite) and <strong><code class="language-plaintext highlighter-rouge">another_js</code></strong>, providing foundational reference for ByteKiller decompression, audio mixer routines, and bytecode dispatch.
    <ul>
      <li><a href="https://github.com/cyxx/rawgl"><em>cyxx/rawgl</em></a></li>
    </ul>
  </li>
  <li><strong><a href="https://www.scummvm.org/">The ScummVM Team</a></strong>: For pioneering retro game engine preservation and Cinematique virtual machine documentation.</li>
  <li><strong><a href="https://github.com/pschatzmann">Phil Schatzmann (pschatzmann)</a></strong>: Author of <strong><code class="language-plaintext highlighter-rouge">ESP32-A2DP</code></strong>, enabling high-performance Bluetooth A2DP audio streaming.</li>
  <li><strong><a href="https://github.com/lovyan03">Lovyan03</a></strong>: Author of <strong><code class="language-plaintext highlighter-rouge">LovyanGFX</code></strong>, the ultra-fast SPI DMA graphics driver powering our 50 FPS ST7789 display.</li>
</ul>

<hr />

<p><em>Another World is © Éric Chahi and Delphine Software / Dotemu. This project is an independent open-source virtual machine engine created for retro computing research and preservation.</em></p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[How I ported Éric Chahi's iconic 1991 cinematic platformer Another World to a $5 ESP32 with 50 FPS vector rendering, custom capacitive touch PCB gamepad, and Bluetooth audio.]]></summary></entry><entry><title type="html">Promptyly: Vibe Coding Local Apps with Git and Hot-Reloading</title><link href="https://tonym128.github.io/2026/06/21/promptyly-prompt-driven-single-page-apps-with-git-and-hot-reload.html" rel="alternate" type="text/html" title="Promptyly: Vibe Coding Local Apps with Git and Hot-Reloading" /><published>2026-06-21T02:00:00+00:00</published><updated>2026-06-21T02:00:00+00:00</updated><id>https://tonym128.github.io/2026/06/21/promptyly-prompt-driven-single-page-apps-with-git-and-hot-reload</id><content type="html" xml:base="https://tonym128.github.io/2026/06/21/promptyly-prompt-driven-single-page-apps-with-git-and-hot-reload.html"><![CDATA[<h1 id="promptyly-tldr">Promptyly tldr;</h1>

<p><img src="/images/2026/06/promptyly-teaser-opt.jpg" alt="Promptyly main dashboard showing the registry of local apps" title="Promptyly Dashboard" /></p>

<p>I built a cross-platform command-line tool and local server that lets you turn prompts into single-page web applications instantly. They run locally, they have automatic Git version control, hot reloading, a built-in state database, and can be easily shared or published.</p>

<p>Take a look at the code - <a href="https://github.com/tonym128/promptyly">Code</a> or watch the <a href="https://youtu.be/iRDXM13fzwc">Intro Video</a></p>

<p>After going deep into AI-driven generation and building things like <a href="/_posts/2025-12-29-peakylight-post-mortem.markdown">Peakylight</a>, I realized that the loop of typing prompts into a browser chat window, copy-pasting code into a local file, and refreshing the page was driving me crazy.</p>

<p>So, I decided to build a developer tool to make vibe coding feel truly native.</p>

<blockquote>
  <p><strong>Special Thanks:</strong> A huge thank you to <strong>Mr Bob</strong> for his beta testing and ideation!</p>
</blockquote>

<hr />

<h2 id="the-problem-the-copy-paste-loop-of-doom">The Problem: The Copy-Paste Loop of Doom</h2>

<p><img src="/images/2026/06/copypaste-struggle-opt.jpg" alt="Frustrated developer tangled in copy-paste strings of code" title="The Copy-Paste Struggle is Real" /></p>

<p>AI models (Gemini, Claude, GPT-4o) are amazing at spitting out complete HTML pages containing CSS, Javascript, and fully functional interactive mockups.</p>

<p>But actually working on them is a drag:</p>
<ol>
  <li>You prompt the LLM: <em>“Write me a Pomodoro timer with ambient rain sounds.”</em></li>
  <li>It outputs 400 lines of HTML/JS/CSS.</li>
  <li>You copy it, create <code class="language-plaintext highlighter-rouge">index.html</code>, paste it, double-click it. It looks good!</li>
  <li>You want to make a change: <em>“Add a dark mode toggle and task list.”</em></li>
  <li>It outputs another 500 lines. You copy it, select all, paste it, refresh.</li>
  <li>The styling broke, or a script failed, and now you have no version history to see what changed.</li>
</ol>

<p>The developer-to-editor feedback loop is broken when you’re writing code through chat windows. There’s no history, no easy local state, and no hot reloading. I wanted to build a “Neuralink” for my local project folder.</p>

<hr />

<h2 id="the-approach-a-custom-deep-link-protocol--local-server">The Approach: A Custom Deep Link Protocol &amp; Local Server</h2>

<p><img src="/images/2026/06/brain-connector-opt.jpg" alt="Circuit brain connected to a terminal prompt screen" title="The Neuralink between LLM and CLI" /></p>

<p>I decided to write <strong>Promptyly</strong> in Go. Why Go? Because I wanted a single, lightweight binary that starts instantly and can handle background servers, custom OS protocols, and git operations without dragging in a heavy runtime.</p>

<p>Here’s how the stack works:</p>

<h3 id="1-the-custom-prompt-scheme">1. The custom <code class="language-plaintext highlighter-rouge">prompt://</code> scheme</h3>
<p>Promptyly registers a custom protocol handler with the OS (macOS, Windows, and Linux). Clicking a link like <code class="language-plaintext highlighter-rouge">prompt://create?prompt=Sleek+Calculator</code> launches the local <code class="language-plaintext highlighter-rouge">promptyly</code> CLI, parses the arguments, and fires up the generation pipeline automatically.</p>

<h3 id="2-git-backed-generations">2. Git-Backed Generations</h3>
<p>Every app you create is initialized in <code class="language-plaintext highlighter-rouge">~/promptyly-apps/&lt;slug&gt;</code> as a local Git repository. Every time you issue an edit instruction via the CLI terminal, the new code is generated, written to disk, and committed to Git automatically. If the model goes off the rails, you can easily check <code class="language-plaintext highlighter-rouge">git diff</code> or rollback.</p>

<h3 id="3-injected-sse-hot-reloading">3. Injected SSE Hot Reloading</h3>
<p>We serve all apps concurrently on a single port (<code class="language-plaintext highlighter-rouge">6071</code>) at <code class="language-plaintext highlighter-rouge">http://localhost:6071/apps/&lt;name&gt;/</code>. The server automatically injects a tiny Server-Sent Events (SSE) listener script into the HTML header. The second the CLI completes an LLM edit, it pings the SSE client, and the page refreshes in the browser instantly.</p>

<h3 id="4-zero-config-state-api">4. Zero-Config State API</h3>
<p>Instead of spinning up a backend (Node, Go, Supabase) for every tiny utility app, Promptyly hosts a built-in JSON database endpoint at <code class="language-plaintext highlighter-rouge">_promptyly/api/db</code> that maps to <code class="language-plaintext highlighter-rouge">.promptyly/db.json</code> in the app’s folder. The frontend can read and write state simply using standard client-side <code class="language-plaintext highlighter-rouge">fetch</code> calls.</p>

<h3 id="5-local-cli-remote-serving--built-in-tunnelling">5. Local CLI, Remote Serving &amp; Built-in Tunnelling</h3>
<p>Promptyly operates as a local CLI communicating with a background service/hosted daemon (running locally on port <code class="language-plaintext highlighter-rouge">6071</code>). For publishing and exploring other developers’ creations, Promptyly connects to a remote registry server hosting a public website on port <code class="language-plaintext highlighter-rouge">6072</code> (the Sharing Registry). If you want to share a local app with a remote user without publishing it publicly first, Promptyly provides built-in Cloudflare Tunneling to securely expose the local instance via a public URL. The tunneling architecture is modular, meaning it can be easily configured to use alternatives like <strong>ngrok</strong> or <strong>DynDNS</strong>.</p>

<hr />

<h2 id="the-learnings">The Learnings</h2>

<p>Building this taught me a few major lessons:</p>

<ul>
  <li><strong>OS Deep Linking is a Maze</strong>: Setting up deep links sounds easy until you try to support macOS (<code class="language-plaintext highlighter-rouge">plist</code> associations), Linux (<code class="language-plaintext highlighter-rouge">xdg-settings</code> and desktop entries), and Windows (Registry Keys). They all handle parameters differently, and testing the edge cases took a significant chunk of time.</li>
  <li><strong>Local Coding Models are Legit</strong>: I built an option to automatically download and run <strong>Qwen2.5-Coder-1.5B</strong> in <code class="language-plaintext highlighter-rouge">llamafile</code> format locally. It runs smoothly on a standard laptop with only 4GB RAM, letting you write and edit apps completely offline without paying for API keys.</li>
  <li><strong>Vibe-first UX matters</strong>: Running an interactive terminal prompt loop directly alongside a hot-reloaded browser tab completely changes the experience. It feels like an extension of your own environment rather than a generic chat page.</li>
</ul>

<hr />

<h2 id="the-good">The Good</h2>

<p><img src="/images/2026/06/wizard-success-opt.jpg" alt="Friendly wizard casting a neon green spell on a computer screen" title="The Magic of Prompt-Driven Editing" /></p>

<ul>
  <li><strong>Frictionless Workflow</strong>: The speed is incredible. You type <code class="language-plaintext highlighter-rouge">.create "sleek expense tracker"</code>, the browser pops open, and within seconds the app is live. If something needs tweaking, you just type it in the terminal (<code class="language-plaintext highlighter-rouge">promptyly&gt; add export to CSV</code>) and watch it update.</li>
  <li><strong>Portable and Packaged</strong>: Because the local JSON database resides inside the app’s folder, you can run <code class="language-plaintext highlighter-rouge">promptyly export &lt;name&gt;</code> to bundle the entire project (code, git history, and your actual data/state) into a single <code class="language-plaintext highlighter-rouge">.zip</code> file. Your friends can import it and it works instantly with the database intact.</li>
  <li><strong>Promptyly Hub</strong>: Serving a beautiful dark-mode dashboard at <code class="language-plaintext highlighter-rouge">http://localhost:6071</code> that lists all your generated apps, their creation prompts, and metadata makes managing local projects very clean.</li>
</ul>

<hr />

<h2 id="the-bad">The Bad</h2>

<p><img src="/images/2026/06/registry-maze-opt.jpg" alt="Confused developer navigating a maze of Windows registry keys" title="Navigating OS Deep Link Settings" /></p>

<ul>
  <li><strong>The Single-Writer DB Bottleneck</strong>: While the local JSON database works wonders for private local tools, it doesn’t scale. If you publish your app to the public Sharing Registry (port <code class="language-plaintext highlighter-rouge">6072</code>), multiple users writing to a single JSON file will immediately hit conflicts.</li>
  <li><strong>Browser Sandbox &amp; Protocol Prompts</strong>: Every time the browser tries to trigger a deep link, it pops up a warning dialogue asking for permission to open Promptyly. It’s a security feature, but it slightly breaks the smooth “web-to-desktop” magic.</li>
  <li><strong>Windows File Locking</strong>: Upgrading the Go binary on the fly is tricky on Windows because running executables are locked. I had to write a PowerShell loopback script that waits for the CLI process to terminate before hot-swapping the executable.</li>
</ul>

<hr />

<h2 id="the-conclusion">The Conclusion</h2>

<p>Promptyly is the developer’s scratchpad I’ve always wanted. It bridges the gap between high-level prompt engineering and local system control.</p>

<p>Instead of configuring configurations, downloading massive npm modules, and spending half an hour setting up boilerplate code for a weekend project, I can just prompt my way to a functional tool in under a minute.</p>

<hr />

<h2 id="the-future">The Future</h2>

<p><img src="/images/2026/06/robot-builders-opt.jpg" alt="Tiny glowing robot builders building a digital castle in cyberspace" title="A Team of Tiny AI Builders" /></p>

<p>Right now, Promptyly edits single files sequentially. The next step is introducing <strong>Agentic Team Collaboration</strong> (similar to what tools like Antigravity do), allowing multiple specialized LLM subagents to edit complex layouts, write tests, and interact with directories in parallel.</p>

<p>I also want to introduce native multi-page routing templates and a standard authentication helper to secure shared registry apps out of the box.</p>

<p>If you want to play around with it, deployment is simplified: you can host the pre-built Docker container for the server/registry and install the local CLI to interact with it. Check out the <a href="https://github.com/tonym128/promptyly">Promptyly GitHub</a> to get started!</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[A post-mortem and review of Promptyly, a CLI and local daemon that turns natural language prompts into fully functional, local, version-controlled web apps.]]></summary></entry><entry><title type="html">SovereignS3nc: Building a Decentralized Network Without a Backend</title><link href="https://tonym128.github.io/2026/05/25/sovereigns3nc-building-a-decentralized-network-without-a-backend.html" rel="alternate" type="text/html" title="SovereignS3nc: Building a Decentralized Network Without a Backend" /><published>2026-05-25T10:00:00+00:00</published><updated>2026-05-25T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/05/25/sovereigns3nc-building-a-decentralized-network-without-a-backend</id><content type="html" xml:base="https://tonym128.github.io/2026/05/25/sovereigns3nc-building-a-decentralized-network-without-a-backend.html"><![CDATA[<h1 id="sovereigns3nc-the-no-backend-experiment">SovereignS3nc: The No-Backend Experiment</h1>

<p>What if you could build a fully functional social network, a blog, or even a banking app, without ever deploying a backend server? No Node.js API, no Go microservices, no Supabase, and no Firebase. Just a static frontend and a bucket of S3 storage.</p>

<p>That was the goal of <strong>SovereignS3nc</strong>. I wanted to see how far I could push the concept of a “sovereign network”—where the user truly owns their data, and the application is just a lens through which they view it.</p>

<h2 id="the-vision-zero-hosted-compute">The Vision: Zero Hosted Compute</h2>

<p>The core idea is simple: your data lives in an S3 bucket (AWS, OCI, or self-hosted via something like <strong>RustFS</strong>). When you use an application, you connect it to <em>your</em> storage. You share data by giving other users permission to read specific parts of your bucket, and you follow others by reading from theirs.</p>

<p>Using a single statically hosted frontend, you should be able to:</p>
<ol>
  <li>Connect to your own data.</li>
  <li>Share with other users.</li>
  <li>Keep everything encrypted and private.</li>
</ol>

<h2 id="pushing-the-limits">Pushing the Limits</h2>

<p>I pushed this idea as far as I could, and while I hit some interesting walls, I also found some elegant ways around them.</p>

<h3 id="what-works-beautifully-private-stores--e2ee">What Works Beautifully: Private Stores &amp; E2EE</h3>

<p>The private side of things is rock-solid. By using local public keys and E2EE (End-to-End Encryption) via <code class="language-plaintext highlighter-rouge">tweetnacl</code>, your data remains secure even if the bucket itself is compromised. Since I don’t allow listing on the S3 bucket, and everything is encrypted with your local key before it even hits the wire, the storage provider is essentially a “dumb” pipe.</p>

<p>The backend storage model I settled on works surprisingly well: <strong>SQLite daily files per user</strong>. These databases are hashed, encrypted, and synced incrementally. It’s clean, it’s fast, and it blends perfectly with the offline-first architecture.</p>

<h3 id="the-stumbling-block-shared-resources">The Stumbling Block: Shared Resources</h3>

<p>The biggest challenge in a serverless, backend-less world is <strong>shared state</strong>.</p>

<p>In SovereignS3nc, I used a <code class="language-plaintext highlighter-rouge">users.json</code> file at a fixed location to handle user discovery. But because multiple users need to write to it (to “register” themselves on the network), it becomes a massive bottleneck. Without a central authority or a backend server to handle user creation and custom access rights, multi-writer shared files are a significant hurdle.</p>

<h2 id="the-demos-alphabeta-ready">The Demos: Alpha/Beta Ready</h2>

<p>I built out a suite of demos that show what’s possible:</p>

<h3 id="social-demo">Social Demo</h3>
<p>A decentralized social network with messaging and feeds.
<img src="/images/2026/05/25/SovereignS3nc-opt.jpg" alt="Social Demo" /></p>

<h3 id="social-bank-banky">Social Bank (Banky)</h3>
<p>A transaction tracker that I actually use for my children’s bank accounts.
<img src="/images/2026/05/25/Banky-opt.jpg" alt="Banky" /></p>

<h3 id="social-blog">Social Blog</h3>
<p>A decentralized blogging platform.
<img src="/images/2026/05/25/Blog-opt.jpg" alt="Social Blog" /></p>

<h3 id="social-board">Social Board</h3>
<p>A shared message board for distributed teams.
<img src="/images/2026/05/25/SovereignBoard-opt.jpg" alt="Social Board" /></p>

<p>All of these are currently at an Alpha/Beta level and demonstrate that for many use cases, the “no-backend” approach is not just a pipe dream.</p>

<h2 id="lessons-learned--the-path-forward">Lessons Learned &amp; The Path Forward</h2>

<p>In the end, SovereignS3nc is a perfect library for creating flexible, usable stores for personal privately hosted applications. For me, it’s the ideal way to host quick apps that I only access via a VPN.</p>

<p>I believe there’s still a gap in the web standards for a native authentication mechanism that can dynamically build access rights on the server side via S3 APIs. The “real” version of this concept is likely close to something like <strong>Supabase</strong>. To move SovereignS3nc forwards to something production ready, the next step would be integrating an identity provider and a minimal server to handle user registration and ACLs.</p>

<p>It was a great exercise in taking a concept to its logical extreme. I particularly love the idea of “pop-up” local networks—networks that exist only for a specific event, hosted entirely on the participants’ own S3 buckets.</p>

<p>How do you keep control without a server? How do you handle access control? I have some answers around encryption and security, but definitely still have a few unanswered questions still too.</p>

<p>Check out the <a href="https://github.com/tonym128/SovereignS3nc">SovereignS3nc GitHub page</a> if you want to dive into the code.</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[How far can you go with just S3 and zero hosted compute? Pushing the limits of sovereign networks with SovereignS3nc.]]></summary></entry><entry><title type="html">Debugging the Novel: How I Built a 9-Book Cyberpunk Series with AI</title><link href="https://tonym128.github.io/2026/04/25/debugging-the-novel-how-i-built-a-9-book-cyberpunk-series-with-ai.html" rel="alternate" type="text/html" title="Debugging the Novel: How I Built a 9-Book Cyberpunk Series with AI" /><published>2026-04-25T10:00:00+00:00</published><updated>2026-04-25T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/04/25/debugging-the-novel-how-i-built-a-9-book-cyberpunk-series-with-ai</id><content type="html" xml:base="https://tonym128.github.io/2026/04/25/debugging-the-novel-how-i-built-a-9-book-cyberpunk-series-with-ai.html"><![CDATA[<h1 id="debugging-the-novel-how-i-built-a-9-book-cyberpunk-series-with-ai">Debugging the Novel: How I Built a 9-Book Cyberpunk Series with AI</h1>

<blockquote>
  <p><strong>Status:</strong> Published<br />
<strong>Genre:</strong> Cyberpunk / Tech-Noir / Non-Fiction<br />
<strong>Tags:</strong> #AIWriting #LLM #CreativeProcess #Cyberpunk #SoftwareEngineering</p>
</blockquote>

<hr />

<div style="display: flex; justify-content: center;">
  <img src="/images/2026/04/1_titled-opt.jpg" width="50%" alt="System Exception - Unhandled Exceptions Book 1 Cover" />
</div>

<h2 id="abstract">Abstract</h2>

<p>As a programmer, my relationship with AI has mostly been about source code, building applications, refactoring functions or hunting down race conditions. Recently, I found myself craving a very specific story: a humorous, sprawling, world-spanning cyberpunk detective noir. I decided to see if I could build it.</p>

<p>What started as a weekend experiment in “creative prompting” turned into a massive project: <strong>The Unhandled Exceptions</strong>, a nine-book series. Along the way, I learned that writing a book with an LLM isn’t just about asking it to “write a story”, it’s about system architecture, context management, and a surprising amount of manual “Meatspace” formatting.</p>

<hr />

<h2 id="phase-1-architecting-the-universe">Phase 1: Architecting the Universe</h2>

<p>With AI, writing the prose is easy; building a world is hard. I spent a significant amount of time in the ideation phase. This wasn’t just shouting into a void; it felt like playing a high-stakes Tabletop RPG with the AI.</p>

<p>I created character sheets that functioned like documentation. We traded plot ideas back and forth, I’d provide a prompt, the AI would suggest three directions, and I’d pick the most interesting “bug” to turn into a feature.</p>

<p>To keep the story consistent across nine books, I had to be disciplined with my “source code”:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">story.md</code>: Mapped out the overall series arc.</li>
  <li><code class="language-plaintext highlighter-rouge">characters.md</code>: Detailed bios and motivation trackers.</li>
  <li><code class="language-plaintext highlighter-rouge">chapters.md</code>: Granular breakdowns of pacing and plot beats.</li>
  <li><code class="language-plaintext highlighter-rouge">synopsis.md</code>: Book synopsis, story points, and chapter-by-chapter one-liner summaries.</li>
</ul>

<p>I wanted to ensure the <strong>“Context Window”</strong> was always present and optimized. If the LLM forgot that Detective Miller hated “Smart” coffee, the immersion broke. I treated these markdown files as the <strong>“State”</strong> of my application.</p>

<hr />

<h2 id="phase-2-the-factory-floor-generation">Phase 2: The Factory Floor (Generation)</h2>

<p>Once the architecture was solid, we moved to production. For the sake of speed and narrative flow, we generated two chapters at a time.</p>

<p>I set a hard constraint: <strong>20 chapters per book.</strong></p>

<p>Mapping the arc for all nine books before writing a single word of Chapter 1 was the only way to ensure that the “Zero Day” finale in Book 9 actually paid off the breadcrumbs dropped throughout the series.</p>

<hr />

<h2 id="phase-3-the-polish-iterative-editing">Phase 3: The Polish (Iterative Editing)</h2>

<p>The first draft is never the final product. The editing process was an iterative Q&amp;A loop. I’d go through the chapters, identify “hallucinations” or repetitive phrasing, and we’d work together to refactor the prose.</p>

<blockquote>
  <p><em>Note: If I saw “chocolate-chip eyes” one more time, I was going to crash the system.</em></p>
</blockquote>

<p>It was less about fixing typos and more about fixing narrative logic.</p>

<hr />

<h2 id="phase-4-logic-gates--lost-characters">Phase 4: Logic Gates &amp; Lost Characters</h2>

<p>Once all the beats and rough edits were in place, I did a lot of work around book length, chapter lengths and character placement. Building skills to monitor the length of chapters and books. Building skill to identify characters in the books and their impact. Expanding in most cases, but a few times during big edits Gemini would occasionally lose the plot.</p>

<p>In one particular instance I had an entire chapter go missing during an unrelated edit, but thankfully due to diligent source control I was able to go back and retrieve it.</p>

<p>I had to spend a substantial amount of time re-integrating missing characters into the books in cases where they were under-represented.</p>

<p>I have immense respect for the authors I love and this process gave me another group of people to be very respectful to as well, proof readers, editors and layout experts (foreshadowing).</p>

<p>It honestly takes a village to grow from a baby manuscript to a book.</p>

<hr />

<h2 id="phase-5-bridging-the-air-gap-publishing">Phase 5: Bridging the Air-Gap (Publishing)</h2>

<p>I decided early on that I wanted to see what it would take to go from a digital idea to a physical object on a shelf. I chose <strong>Amazon KDP (Kindle Direct Publishing)</strong> for its simplicity.</p>

<p>Amazon handles the heavy lifting: distribution, printing on demand, and providing the necessary ISBNs and ASINs. They take their commission and product fees, which, for a side project like this, felt like a fair trade for the infrastructure they provide.</p>

<h3 id="the-hidden-boss-formatting">The Hidden Boss: Formatting</h3>

<p>If you think writing 180 chapters is hard, try getting page numbers to behave in a 400-page PDF. Formatting took a massive chunk of the post-writing schedule. You have to juggle two different <strong>“build targets”</strong>:</p>

<ol>
  <li><strong>E-book</strong>: Requires an EPUB with specific Amazon-friendly formatting.</li>
  <li><strong>Paperback</strong>: Requires a precisely sized PDF.</li>
</ol>

<p>Things like “Chapter starts on a new page,” “Footnote placement,” and “Page numbering” became my new “segmentation faults.” Even the covers were a challenge, an e-book cover is a simple JPEG, but a print cover is a complex PDF “wrap” that includes the spine width, which changes based on your page count.</p>

<hr />

<h2 id="final-thoughts">Final Thoughts</h2>

<p>Is it “cheating” to write with AI? its definitely different, but I don’t think so. I feel more like a <strong>Director</strong> or a <strong>Lead Architect</strong>. I provided the vision, the constraints, and the “human” soul, while the AI provided the raw processing power to flesh out the world.</p>

<p>I ended up with something I actually wanted to read. And in the process, I gained a massive amount of respect for the technical hurdles that traditional authors face every day.</p>

<hr />

<h2 id="-tips-for-ai-authors">💡 Tips for AI-Authors</h2>

<ul>
  <li><strong>Markdown is your friend.</strong> It’s clean, version-control-friendly, and LLMs understand it perfectly.</li>
  <li><strong>Plan the end first.</strong> If you don’t know the finale of your series and work towards it, the AI will just meander.</li>
  <li><strong>Don’t skip the formatting.</strong> Use the Kindle Desktop Publishing builder, it’s a lifesaver for the cover wraps.</li>
</ul>

<hr />

<p><em>The Unhandled Exceptions first 3 books are available on Amazon. Not bad for a project that started with a “What if?” prompt.</em></p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Book 1 - System Exception</th>
      <th style="text-align: center">Book 2 - Memory Leak</th>
      <th style="text-align: center">Book 3 - FATAL ERROR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><a href="https://www.amazon.com/System-Exception-Unhandled-Exceptions-Book-ebook/dp/B0GNMRWSYD"><img src="/images/2026/04/1_titled-opt.jpg" width="200" alt="System Exception Cover" /></a></td>
      <td style="text-align: center"><a href="https://www.amazon.com/Memory-Leak-Unhandled-Exceptions-Book-ebook/dp/B0GS9BSHLD"><img src="/images/2026/04/2_titled-opt.jpg" width="200" alt="Memory Leak Cover" /></a></td>
      <td style="text-align: center"><a href="https://www.amazon.com/gp/product/B0GX2YF9MX"><img src="/images/2026/04/3_titled-opt.jpg" width="200" alt="Fatal Error Cover" /></a></td>
    </tr>
  </tbody>
</table>

<p>And more to come, see you in Neo-Viridia!</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[How a weekend experiment turned into building a nine-book cyberpunk series using AI and LLMs.]]></summary></entry><entry><title type="html">Sticks of Rage: Scaling Up to a 2D Brawler</title><link href="https://tonym128.github.io/2026/04/24/sticks-of-rage-arduboy.html" rel="alternate" type="text/html" title="Sticks of Rage: Scaling Up to a 2D Brawler" /><published>2026-04-24T10:00:00+00:00</published><updated>2026-04-24T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/04/24/sticks-of-rage-arduboy</id><content type="html" xml:base="https://tonym128.github.io/2026/04/24/sticks-of-rage-arduboy.html"><![CDATA[<h1 id="sticks-of-rage-scaling-up-to-a-2d-brawler">Sticks of Rage: Scaling Up to a 2D Brawler</h1>

<p><img src="/images/2026/05/10/frontscreen-opt.jpg" alt="Front Screen" /></p>

<p>After the success of <em>Stick Fighter</em>, I found myself deeply attached to its core engine. The bone-based skeletal animation system I had engineered for the Arduboy felt like it had more to give. I started wondering: <em>could this be the backbone of a classic 2D brawler?</em></p>

<p>The result is <strong>Sticks of Rage</strong>, an evolution of the <em>Stick Fighter</em> tech stack that transformed a 1v1 fighter into a chaotic, side-scrolling beat ‘em up.</p>

<p>View its game page and play it online <a href="https://community.arduboy.com/t/sticks-of-rage/13420">here</a></p>

<p>Take a look at the source code <a href="https://github.com/tonym128/sticksofrage">here</a></p>

<h2 id="technology-reuse-the-stick-engine">Technology Reuse: The “Stick Engine”</h2>

<p><img src="/images/2026/05/10/fighter-opt.jpg" alt="Fighter Select" /></p>

<p>The animation system migrated to the new project almost seamlessly. By reusing the core skeletal pose data, I was able to hit the ground running. I’ve tentatively dubbed this codebase the “Stick Engine.” While it started as a 1:1 port of the <em>Stick Fighter</em> logic, it has since been heavily refactored to support the more complex demands of a side-scrolling world.</p>

<h2 id="the-evolution-of-the-system">The Evolution of the System</h2>

<p>Taking a 1v1 engine and making a brawler required adding several new layers of depth:</p>

<ul>
  <li><strong>Stat-Based Gameplay:</strong> I introduced a simple RPG-lite stat system for Life, Damage, and Speed. This allowed for diverse character archetypes: big enemies hit harder and have more health but move sluggishly, while smaller ones are fast but frail.</li>
  <li><strong>Parallax Backgrounds:</strong> To give the flat screen a sense of depth, I implemented a layered parallax background system. Even with simple geometric lines, the movement of different layers really sells the “traveling through a city” vibe.</li>
  <li><strong>Plane Movement:</strong> Unlike <em>Stick Fighter</em>, which locked characters to a single horizontal line, <em>Sticks of Rage</em> allows players to move up and down in the plane, opening up the battlefield for strategic positioning.</li>
</ul>

<h2 id="character-dynamics-and-story">Character Dynamics and Story</h2>

<p><img src="/images/2026/05/10/story-opt.jpg" alt="Story Screen" /></p>

<p>I took the nine original characters from <em>Stick Fighter</em> and re-purposed them for this new world. I chose three main protagonists for the player to master, while the remaining six were converted into distinct enemy archetypes. Each playable character now comes with their own intro and outro stories, fleshed out with “boss talk” dialogue that triggers before the big fights.</p>

<h2 id="the-ai-challenge-swarming">The AI Challenge: Swarming</h2>

<p><img src="/images/2026/05/10/gameplay-opt.jpg" alt="Gameplay" /></p>

<p>One of the biggest shifts was moving from 1v1 combat to swarming. Enemies now use a group-based AI, employing different “personalities” to keep the player on their toes. As stages get harder, the swarm density and aggression increase.</p>

<p>However, this came at a cost. The ATMega32U4 is not a powerhouse. Getting multiple enemies, parallax layers, and player combat running simultaneously meant the CPU was constantly on the edge of a collapse. I had to enforce a strict limit: <strong>a maximum of four characters on screen at once.</strong> Keeping the background geometry simple was the only way to squeeze the extra CPU cycles out of the hardware to maintain playability.</p>

<h2 id="the-hardest-part-the-memory-wall">The Hardest Part: The Memory Wall</h2>

<p>The hardest part of this project, once again, was fitting it all in. <em>Sticks of Rage</em> pushed the memory and CPU constraints even further than its predecessor. Every byte saved in the background engine was a byte used for a character stat or a line of story dialogue.</p>

<h2 id="reflecting-on-re-use">Reflecting on Re-use</h2>

<p>Building <em>Sticks of Rage</em> taught me that technical re-use is one of the most rewarding parts of software engineering. Watching a system I built for one purpose evolve to serve a completely different genre—all while staying within the same 8-bit constraints—was an incredible experience.</p>

<p>The game may be simple, but seeing three playable characters roaming a parallax city, battling swarms of enemies, all on a device with less power than a modern calculator? That’s the kind of technical satisfaction that keeps me building on the Arduboy.</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[How I took the engine from Stick Fighter and evolved it into a full-scale 2D brawler, complete with parallax backgrounds, swarming AI, and a new RPG-lite stat system.]]></summary></entry><entry><title type="html">Stick Fighter: Fighting the Arduboy’s Constraints</title><link href="https://tonym128.github.io/2026/04/23/stick-fighter-arduboy.html" rel="alternate" type="text/html" title="Stick Fighter: Fighting the Arduboy’s Constraints" /><published>2026-04-23T10:00:00+00:00</published><updated>2026-04-23T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/04/23/stick-fighter-arduboy</id><content type="html" xml:base="https://tonym128.github.io/2026/04/23/stick-fighter-arduboy.html"><![CDATA[<h1 id="stick-fighter-fighting-the-arduboys-constraints">Stick Fighter: Fighting the Arduboy’s Constraints</h1>

<p><img src="/images/2026/05/01/start-opt.jpg" alt="Start Screen" /></p>

<p>When I first saw the Arduboy, I was immediately drawn to its charm and simplicity.</p>

<p>Over the years I’ve really enjoyed its community, making games for it and the big library of games available, the small system with its ability to code for so easily and its online community always bring me back for more.</p>

<p>I did however notice a hole in its library: there were no real-time 2D fighting games. This always bugged me, the platform was crying out for a competitive, fluid brawler, and I decided to take on the challenge.</p>

<p>View its game page and play it online <a href="https://community.arduboy.com/t/stick-fighter/13392">here</a></p>

<p>Take a look at the source code <a href="https://github.com/tonym128/stickfighter">here</a></p>

<h2 id="the-vision-vs-the-reality">The Vision vs. The Reality</h2>

<p><img src="/images/2026/05/01/character_select-opt.jpg" alt="Character Select" /></p>

<p>My goal was ambitious: a fast-paced 2D fighter featuring fluid animations, varied moves, and tight gameplay. I wanted something that felt like a simplified <em>Street Fighter</em> but fit into the tiny footprint of the Arduboy’s ATMega32U4.</p>

<p>The development process was a constant tug-of-war between my design aspirations and the hardware’s strict limits. I started with grand ideas of complex combo systems, but ended up with a refined, high-intensity brawler that emphasized spacing and timing, a compromise that arguably made the final game much more enjoyable to play.</p>

<h2 id="sprites-vs-bone-systems-a-deep-dive">Sprites vs. Bone Systems: A Deep Dive</h2>

<p>Early on, I had to make a critical decision: use traditional frame-by-frame sprites or a procedural bone system. Traditional sprites were memory-hungry. Loading multiple frames for different moves for even two characters would have exhausted the flash memory before I even wrote the game loop. I chose a <strong>bone-based skeletal animation system</strong>.</p>

<h3 id="traditional-3d-vs-stripped-down-2d">Traditional 3D vs. Stripped-Down 2D</h3>

<p><img src="/images/2026/05/01/gameplay_far-opt.jpg" alt="Gameplay Far" /></p>

<p>In <strong>traditional 3D animation</strong>, you have a hierarchy of bones, a complex skinning system, and often a root node that controls everything. This requires high-precision floating-point math and substantial RAM.</p>

<p><strong>My system for <em>Stick Fighter</em> takes the exact opposite approach:</strong></p>

<ol>
  <li><strong>No Mesh Skinning:</strong> Stick figures are just lines. I don’t need to skin a mesh, only render lines between endpoints.</li>
  <li><strong>No Root Hips:</strong> Instead of a complex root-node hierarchy, I use a simplified, linear connection chain.</li>
  <li><strong>Fixed Plane:</strong> Because the movement is restricted to a 2D plane, I avoid 3D rotations (quaternions) entirely. Every bone is represented simply by a start position and an angle.</li>
</ol>

<h3 id="simplifying-the-geometry">Simplifying the Geometry</h3>

<p>To avoid the performance hit of complex inverse kinematics (IK), I made two critical trade-offs:</p>
<ul>
  <li><strong>Constant Lengths:</strong> Since we are in 2D, the distance between joints never changes. This means I don’t need to calculate scaling factors. Each bone length is a pre-defined constant.</li>
  <li><strong>Reduced Joint Complexity:</strong> By keeping the hierarchy shallow, I eliminate the need for recursive matrix multiplication.</li>
</ul>

<h3 id="the-mathematical-cheat">The Mathematical “Cheat”</h3>

<p>In <em>Stick Fighter</em>, I use a <strong>forward-looking pose definition</strong>. Each bone’s angle is stored relative to its parent bone. To draw a limb, I take the parent’s world position, add a simplified trigonometric calculation (using a pre-computed sine/cosine lookup table), and draw the line. Because I don’t perform complex matrix inversions or heavy transformations, the math remains manageable even for the Arduboy’s 16MHz processor.</p>

<h2 id="the-technical-hurdle-speed-and-space">The Technical Hurdle: Speed and Space</h2>

<p><img src="/images/2026/05/01/gameplay_close-opt.jpg" alt="Gameplay Close" /></p>

<p>Implementing this on an 8-bit chip is deceptively difficult. I had to ditch standard <code class="language-plaintext highlighter-rouge">float</code> math for fixed-point arithmetic, and build a custom animation engine that could update character positions within the tight frame budget of the Arduboy’s screen refresh. The tooling was another challenge; I had to build a custom animation editor (in SDL) to preview and export these skeletal data structures, essentially building the game’s pipeline from the ground up.</p>

<h2 id="under-the-hood-hitboxes-and-ai">Under the Hood: Hitboxes and AI</h2>

<ul>
  <li><strong>Hit Detection:</strong> To save space, hit detection uses simple Axis-Aligned Bounding Boxes (AABB) relative to the bones. Since the characters are stick figures, I could use their limb vectors to create simple, fast-to-calculate collision zones.</li>
  <li><strong>Enemy AI:</strong> The AI uses a simple Finite State Machine (FSM). It evaluates the distance to the player and decides between aggressive “rush down” states or defensive “spacing” states.</li>
</ul>

<h2 id="everyone-is-a-bunch-of-lines">Everyone is a Bunch of Lines</h2>

<p>To make each character stand out despite being simple stick figures, I developed a modular “head system.”</p>

<p>Instead of drawing a static circle for a head, I designed a set of small geometric primitives—simple line segments and points—that could be configured for each character. By varying the position of these “facial features” (like eye placement, hair spikes, or head accessories) and storing them as a tiny list of offsets, I could give each fighter a distinct silhouette.</p>

<p>Because these were just defined by a few coordinate pairs relative to the head’s bone, this system consumed almost zero additional memory, allowing me to inject unique personality into every character while keeping the animation overhead identical to a standard stick figure.</p>

<h2 id="developing-for-the-desktop-the-sdl-wrapper">Developing for the Desktop: The SDL Wrapper</h2>

<p>To speed up my iteration time, I built a custom SDL wrapper that allowed me to run the game logic directly on Windows. This acted as a virtual development environment for the Arduboy code.</p>

<h3 id="the-pros-rapid-iteration">The Pros: Rapid Iteration</h3>
<ul>
  <li><strong>Faster Compilation:</strong> I could build a binary and test changes in seconds, bypassing the slow build and upload process to the physical microcontroller.</li>
  <li><strong>Rapid Prototyping:</strong> It was invaluable for tweaking “juice” elements—like gravity, character movement speed, or animation frames—allowing me to get the “feel” pixel-perfect with ease.</li>
</ul>

<h3 id="the-cons-the-hardware-disconnect">The Cons: The Hardware Disconnect</h3>
<ul>
  <li><strong>Input Differences:</strong> Keyboard input is fundamentally different from the gamepad and buttons of the Arduboy. I often found myself over-reacting or pulling off combos that were impossible on the real hardware.</li>
  <li><strong>False Sense of Performance:</strong> The biggest trap was the lack of hardware constraints. I could easily blow past the 32KB flash memory limit or the tiny RAM budget without realizing it. On desktop, the game always ran at a perfect 60FPS, leaving me completely unaware that the Arduboy might actually be stuttering or running out of memory.</li>
</ul>

<h3 id="balancing-the-approach">Balancing the Approach</h3>
<p>To mitigate these risks, I adopted a strict “touch-base” policy. I would frequently flash the binary onto the actual console to check performance and memory usage. While emulators or regular hardware testing are necessary, the development speed gains from an SDL wrapper are absolutely worth the extra vigilance. If you pay attention to the hardware constraints, this hybrid workflow is an incredibly powerful tool.</p>

<h2 id="features-dropped-and-lessons-learned">Features Dropped and Lessons Learned</h2>

<p>Not every idea survived. I had plans for complex stage hazards and a multi-tiered background, but they were cut to prioritize the core gameplay loop.</p>

<p>Playing <em>Stick Fighter</em> feels rewarding because of the “juice” added late in development:</p>
<ul>
  <li><strong>Dynamic Zooming:</strong> The camera scales and zooms based on character distance, adding cinematic flair.</li>
  <li><strong>Character Scaling:</strong> Using procedural transformations to emphasize weight.</li>
  <li><strong>Fireballs:</strong> Adding simple projectile physics gave the game the “spacing” element it desperately needed.</li>
</ul>

<h2 id="the-story-and-the-wall">The Story and the “Wall”</h2>

<p><img src="/images/2026/05/01/ladder-opt.jpg" alt="Ladder" /></p>

<p>Despite the technical focus, I wanted the game to have personality. I ended up adding a surprisingly large amount of story text to character introductions, giving each stick fighter their own motivations.</p>

<p>There is a unique kind of fun in developing under extreme constraints. When you hit the “memory wall,” you’re forced to stop adding features and start polishing what you have. You can’t just throw more code at the problem—you have to optimize, rethink, and simplify.</p>

<p><em>Stick Fighter</em> was a masterclass in this philosophy. It was a challenging project, but very rewarding as well.</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[The story behind building a 2D fighting game for the Arduboy. Tackling bone systems, tight memory limits, and the art of constraint-based game design.]]></summary></entry><entry><title type="html">Axis Rush: High-Speed Cylindrical Racing in Three.js</title><link href="https://tonym128.github.io/2026/03/17/axis-rush-high-speed-cylindrical-racing.html" rel="alternate" type="text/html" title="Axis Rush: High-Speed Cylindrical Racing in Three.js" /><published>2026-03-17T10:00:00+00:00</published><updated>2026-03-17T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/03/17/axis-rush-high-speed-cylindrical-racing</id><content type="html" xml:base="https://tonym128.github.io/2026/03/17/axis-rush-high-speed-cylindrical-racing.html"><![CDATA[<h1 id="axis-rush-tldr">Axis Rush tldr;</h1>

<p>I’ve just released <strong>Axis Rush</strong>, a futuristic arcade racer inspired by the classics like <em>F-Zero</em> and <em>Wipeout</em>. The twist? You’re racing on a giant cylinder, and you can flip between the inside and outside of the track at will to dodge obstacles or find the perfect racing line.</p>

<p>Take a look at the demo page - <a href="https://tonym128.github.io/axis_rush/">Live Demo</a></p>

<p>Take a look at the code - <a href="https://github.com/tonym128/axis_rush">GitHub Repository</a></p>

<p><img src="/images/axis-rush/axis-rush-opt.jpg" alt="Axis Rush main menu showing a futuristic racing craft on a cylindrical track" title="Axis Rush Main Menu" /></p>

<h2 id="the-vision-redefining-the-racing-line">The Vision: Redefining the Racing Line</h2>

<p>In most racers, the “line” is two-dimensional. In <strong>Axis Rush</strong>, its a full 360-degree experience. By allowing players to jump between the <strong>Interior</strong> and <strong>Exterior</strong> of the cylindrical track, I wanted to create a game where spatial awareness is just as important as reflexes.</p>

<p>Flipping to the inside gives you a tighter turn radius but limits your visibility, while the outside offers a grand view of the neon-soaked horizon but leaves you exposed.</p>

<h2 id="features-beyond-the-speed-limit">Features: Beyond the Speed Limit</h2>

<ul>
  <li><strong>Inside/Outside Transition:</strong> Use the <code class="language-plaintext highlighter-rouge">Space</code> bar to phase through the track. It’s not just a visual trick—steering is dynamically inverted while inside to keep the controls intuitive from the player’s perspective.</li>
  <li><strong>Procedural Aesthetics:</strong> Every texture in the game is baked at runtime using <a href="https://github.com/tonym128/texgen">TexGenJS</a>. I used natural language prompts like “neon circuit” and “carbon fiber” to generate GLSL-based textures, keeping the initial bundle size incredibly small.</li>
  <li><strong>High-Inertia Physics:</strong> I implemented a “slippery” steering model with an angular velocity system. It feels less like driving a car and more like piloting a high-speed hovercraft with massive momentum.</li>
  <li><strong>9 Unique Pilots:</strong> From the disgraced military pilot <strong>Axel Rush</strong> to the digital consciousness <strong>Korvath</strong>, each character has a back-story and a signature color that bleeds into their craft’s neon trails.</li>
  <li><strong>Thumping Techno Engine:</strong> The soundtrack isn’t just a static MP3. its a custom-coded Web Audio engine that generates procedural techno beats using oscillators and noise buffers, reacting to your speed.</li>
</ul>

<h2 id="tech-deep-dive-threejs-and-post-processing">Tech Deep Dive: Three.js and Post-Processing</h2>

<h3 id="1-the-cylindrical-coordinate-system">1. The Cylindrical Coordinate System</h3>
<p>Collision detection on a bending, twisting tube is tricky. Instead of standard XYZ coordinates, I used <strong>T-space</strong> (progress along the spline) and <strong>Angle-space</strong> (rotation around the tube). This makes checking if a vehicle is “on the track” or hitting a boost pad a simple 2D range check.</p>

<h3 id="2-post-processing-pipeline">2. Post-Processing Pipeline</h3>
<p>To capture that “retro-futuristic” vibe, I used the <code class="language-plaintext highlighter-rouge">postprocessing</code> library to layer:</p>
<ul>
  <li><strong>Motion Blur:</strong> To emphasize the extreme speeds.</li>
  <li><strong>Chromatic Aberration:</strong> To simulate the visual distortion of a high-G cockpit.</li>
  <li><strong>Pixelation:</strong> A subtle toggle that gives it that late-90s arcade feel.</li>
</ul>

<h3 id="3-dynamic-fov-and-camera-logic">3. Dynamic FOV and Camera Logic</h3>
<p>As you hit boost pads or enter a slipstream, the camera’s Field of View (FOV) warps. It’s a classic trick, but combined with Three.js’s camera shake and a “zoom-in” countdown, it creates a visceral sense of acceleration.</p>

<h2 id="things-i-was-very-happy-with">Things I was very happy with</h2>

<ul>
  <li><strong>The Transition Mechanic:</strong> Getting the camera to smoothly follow the vehicle as it flips from the inside to the outside of the tube without causing motion sickness was a major win.</li>
  <li><strong>TexGen Integration:</strong> Seeing a complex, glowing track texture appear from just a few lines of GLSL and a keyword prompt still feels like magic.</li>
  <li><strong>The Audio Engine:</strong> Generating a convincing “kick drum” and “hi-hat” purely with code was a fun challenge that paid off in a soundtrack that never sounds exactly the same twice.</li>
</ul>

<h2 id="things-i-wasnt-very-happy-with">Things I wasn’t very happy with</h2>

<ul>
  <li><strong>AI Pathfinding:</strong> The AI currently follows a fairly rigid path. While they can dodge obstacles, they don’t yet “think” tactically about flipping inside/outside to overtake the player.</li>
  <li><strong>Collision Response:</strong> High-speed collisions between vehicles can sometimes result in “jitter” if the physics steps aren’t perfectly aligned with the frame rate. I’m looking into implementing a sub-stepping physics loop to smooth this out.</li>
</ul>

<h2 id="whats-next">What’s Next?</h2>

<p>I’m working on a <strong>League Mode</strong> with a persistent leaderboard and potentially a <strong>Track Editor</strong> that allows players to draw their own splines in 3D space.</p>

<p>If you want to try your hand at the 2026 League, head over to the <a href="https://github.com/tonym128/axis_rush">GitHub repo</a> and give it a spin!</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[Releasing Axis Rush, a futuristic 3D arcade racer built with Three.js featuring cylindrical tracks.]]></summary></entry><entry><title type="html">triangleVision: Real-time Geometric Video Abstraction</title><link href="https://tonym128.github.io/2026/03/08/trianglevision-real-time-geometric-video-abstraction.html" rel="alternate" type="text/html" title="triangleVision: Real-time Geometric Video Abstraction" /><published>2026-03-08T10:00:00+00:00</published><updated>2026-03-08T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/03/08/trianglevision-real-time-geometric-video-abstraction</id><content type="html" xml:base="https://tonym128.github.io/2026/03/08/trianglevision-real-time-geometric-video-abstraction.html"><![CDATA[<h1 id="trianglevision-tldr">triangleVision tldr;</h1>

<p>I built <strong>triangleVision</strong>, a high-performance video processing engine that transforms standard video or live webcam feeds into stylized, artistic triangle meshes in real-time. It uses intelligent point sampling to preserve detail while creating a unique “low-poly” aesthetic.</p>

<p>Take a look at the demo page - <a href="https://tonym128.github.io/triangleVision">Live Demo</a></p>

<p>Take a look at the code - <a href="https://github.com/tonym128/triangleVision">Code</a></p>

<h2 id="the-vision-turning-pixels-into-geometry">The Vision: Turning Pixels into Geometry</h2>

<p>Most video filters operate purely on pixels. <strong>triangleVision</strong> takes a different approach by treating the video frame as a dynamic coordinate space for geometry. The goal was to create a tool that doesn’t just “blur” or “pixelate” but actually re-interprets the visual data through the lens of computational geometry.</p>

<p>By using Delaunay triangulation, the video becomes a living, breathing mesh of interconnected triangles that shift and morph as the underlying scene changes.</p>

<h2 id="features-more-than-just-random-points">Features: More Than Just Random Points</h2>

<p>The magic of triangleVision isn’t just in the triangles themselves, but in how it decides where to place them:</p>

<ul>
  <li><strong>Intelligent Point Sampling:</strong> Instead of a uniform grid, the system uses edge detection (Shi-Tomasi) to place more detail in complex areas and fewer triangles in flat regions.</li>
  <li><strong>Human-Centric Focus:</strong> Integrated HOG and Haar Cascades ensure that faces and human figures retain high fidelity, even in a highly abstracted mesh.</li>
  <li><strong>Motion-Aware Density:</strong> The engine tracks movement, increasing the triangle count in areas with significant action to capture every nuance of motion.</li>
  <li><strong>Custom <code class="language-plaintext highlighter-rouge">.triv</code> Codec:</strong> I designed a bespoke binary format to store triangle data efficiently, allowing for much smaller file sizes than traditional video when storing geometric abstractions.</li>
  <li><strong>Aesthetic Modes:</strong> Beyond the standard mesh, it includes a <strong>Rotoscope</strong> mode for an “ink-and-paint” look and a <strong>Heatmap</strong> mode for technical visualization.</li>
</ul>

<h2 id="tech-deep-dive-python-meets-the-gpu">Tech Deep Dive: Python Meets the GPU</h2>

<h3 id="1-the-processing-pipeline">1. The Processing Pipeline</h3>
<p>The core engine is written in Python, but it relies on <strong>ModernGL</strong> and custom <strong>GLSL shaders</strong> to handle the heavy lifting of rendering. This allows the system to maintain 30+ FPS even while performing complex geometric calculations.</p>

<h3 id="2-high-speed-triangulation">2. High-Speed Triangulation</h3>
<p>We use <strong>SciPy</strong> for the Delaunay triangulation, but to keep things fast, we leverage <strong>Numba</strong> for Just-In-Time (JIT) compilation of our color sampling and data processing loops. This turns bottlenecked Python code into machine-speed execution.</p>

<h3 id="3-multi-threaded-capture">3. Multi-Threaded Capture</h3>
<p>To ensure zero lag in the webcam feed, triangleVision uses a threaded capture system. This decouples the video input from the processing and rendering cycles, preventing any “hitchy” frames during live use.</p>

<h2 id="things-i-was-very-happy-with">Things I was very happy with</h2>

<ul>
  <li><strong>Real-time Performance:</strong> Getting Delaunay triangulation to run at 30+ FPS on 1080p video was a significant hurdle, and seeing it run smoothly is incredibly satisfying.</li>
  <li><strong>The <code class="language-plaintext highlighter-rouge">.triv</code> Player:</strong> The web-based player (JS/HTML) works surprisingly well, proving that the geometric data format is portable across different stacks.</li>
  <li><strong>Stylized Export:</strong> Being able to export the final result back to MP4 or MKV makes it a practical tool for content creators, not just a tech demo.</li>
</ul>

<h2 id="things-i-wasnt-very-happy-with">Things I wasn’t very happy with</h2>

<ul>
  <li><strong>CPU Bottlenecks:</strong> While the rendering is on the GPU, the actual triangulation calculation is still a CPU-heavy task. For ultra-high point counts (10,000+), the frame rate still takes a hit.</li>
  <li><strong>Dependency Chain:</strong> The setup requires a specific set of high-performance libraries (OpenCV, ModernGL, SciPy). I’m looking into ways to package this more easily for non-technical users.</li>
</ul>

<h2 id="whats-next">What’s Next?</h2>

<p>I’m currently experimenting with a <strong>Vulkan-based</strong> implementation of the triangulation itself to move even more of the logic onto the GPU. I also want to add more interactive controls for VJs, such as MIDI support to trigger mesh density and color shifts during live performances.</p>

<p>If you’re into creative coding or computer vision, check out the <a href="https://github.com/tonym128/triangleVision">GitHub repo</a> and let me know what you think!</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[Building a high-performance web tool that transforms video feeds into stylized, real-time triangle meshes.]]></summary></entry><entry><title type="html">TexGen: Procedural Texture Generation for the Web</title><link href="https://tonym128.github.io/2026/03/04/texgen-procedural-texture-generator.html" rel="alternate" type="text/html" title="TexGen: Procedural Texture Generation for the Web" /><published>2026-03-04T10:00:00+00:00</published><updated>2026-03-04T10:00:00+00:00</updated><id>https://tonym128.github.io/2026/03/04/texgen-procedural-texture-generator</id><content type="html" xml:base="https://tonym128.github.io/2026/03/04/texgen-procedural-texture-generator.html"><![CDATA[<h1 id="texgen-tldr">TexGen tldr;</h1>

<p>I built <strong>TexGen</strong>, a lightweight (~15kb) JavaScript library that leverages GLSL shaders to generate high-quality, procedural textures on the fly, including a natural language interface called “TexGen Words”.</p>

<p>Take a look at the site - <a href="https://tonym128.github.io/texgen/">Live Demo</a></p>

<p>Take a look at the code - <a href="https://github.com/tonym128/texgen">Code</a></p>

<h2 id="the-vision-visuals-with-code-not-bandwidth">The Vision: Visuals with Code, Not Bandwidth</h2>

<p>In the modern web, we often find ourselves shipping megabytes of static assets. <strong>TexGen</strong> was born out of a desire to flip that script. Why ship a 2MB PNG of a brick wall when you can ship a 1KB shader string that generates that same wall (and infinite variations of it) directly on the user’s GPU?</p>

<p>Whether you need static baked images for a UI or real-time animated backgrounds for a game, TexGen provides the primitives to build them without the bandwidth bloat.</p>

<h2 id="features-beyond-just-noise">Features: Beyond Just Noise</h2>

<p>TexGen isn’t just a wrapper for a canvas; its a full-featured texture pipeline:</p>

<ul>
  <li><strong>Live GLSL Editor:</strong> A built-in IDE with real-time feedback and @slider annotations for interactive debugging.</li>
  <li><strong>TexGen Words:</strong> An addon that allows you to synthesize complex textures using natural language. Phrases like “blue fire spiral warp” or “purple plasma vortex” are instantly converted into optimized GLSL.</li>
  <li><strong>PBR Ready:</strong> Native support for Albedo, Normal, Roughness, Metallic, and Ambient Occlusion maps.</li>
  <li><strong>Mobile First:</strong> Adaptive scaling for high-DPI screens and high-precision defaults to ensure consistency across mobile GPUs.</li>
  <li><strong>Infinite Variety:</strong> By tweaking a single seed uniform, you can generate millions of unique assets from a single shader.</li>
</ul>

<h2 id="tech-deep-dive-gpu-accelerated-pipelines">Tech Deep Dive: GPU-Accelerated Pipelines</h2>

<h3 id="1-the-power-of-fbm-and-voronoi">1. The Power of FBM and Voronoi</h3>
<p>At its core, TexGen provides highly optimized GLSL implementations of Fractional Brownian Motion (FBM), Voronoi noise, and standard Perlin noise. These are the building blocks of nature—from the way clouds form to the cracks in a dry lake bed.</p>

<h3 id="2-compact-payloads">2. Compact Payloads</h3>
<p>One of the most powerful features is the URL-based sharing. Because the textures are just code, a complex atmospheric sky can be reduced to a tiny Base64 string. This “Texture Streaming” approach allows for massive world-building with minimal initial load times.</p>

<h3 id="3-asynchronous-baking">3. Asynchronous Baking</h3>
<p>To keep the UI responsive, TexGen supports offloading the generation to Web Workers using <code class="language-plaintext highlighter-rouge">OffscreenCanvas</code>. This ensures that even high-resolution 4K bakes won’t freeze the main thread.</p>

<h2 id="things-i-was-very-happy-with">Things I was very happy with</h2>

<ul>
  <li><strong>TexGen Words Synthesis:</strong> Seeing natural language keywords like <code class="language-plaintext highlighter-rouge">lava</code>, <code class="language-plaintext highlighter-rouge">neon</code>, and <code class="language-plaintext highlighter-rouge">glitch</code> successfully combine into a working shader feels like magic.</li>
  <li><strong>The Example Library:</strong> I managed to pack over 50 built-in examples into the gallery, ranging from “Rusted Hull” to “Infinite Island”.</li>
  <li><strong>The TypeScript Support:</strong> Adding full <code class="language-plaintext highlighter-rouge">.d.ts</code> definitions and a dedicated TypeScript gallery makes the library a joy to use in modern dev environments.</li>
</ul>

<h2 id="things-i-wasnt-very-happy-with">Things I wasn’t very happy with</h2>

<ul>
  <li><strong>GLSL Debugging:</strong> While the editor is great, debugging shader logic is still a hurdle for those not familiar with GPU programming. I’m looking into more “visual node” based editing.</li>
  <li><strong>Precision Quirks:</strong> Even with <code class="language-plaintext highlighter-rouge">highp</code> defaults, some older mobile GPUs still exhibit tiny artifacts in heightmaps that are hard to squash without sacrificing performance.</li>
</ul>

<h2 id="showcase-procedural-power-in-practice">Showcase: Procedural Power in Practice</h2>

<p>To demonstrate the versatility of TexGen, I’ve built a series of interactive demos that push the library to its limits. Each one highlights a different core feature while maintaining an incredibly small footprint.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Project</th>
      <th style="text-align: left">Feature Highlight</th>
      <th style="text-align: left">Built-in Textures</th>
      <th style="text-align: left">Payload Size</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/typescript_gallery/index.html">TypeScript Gallery</a></strong></td>
      <td style="text-align: left">Type-safe baking &amp; real-time animation</td>
      <td style="text-align: left">5</td>
      <td style="text-align: left">1.1 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/texture_streaming/index.html">Texture Streaming</a></strong></td>
      <td style="text-align: left">Async Web Workers &amp; OffscreenCanvas</td>
      <td style="text-align: left">1</td>
      <td style="text-align: left">1.6 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/multipass_post/index.html">Multi-pass Composition</a></strong></td>
      <td style="text-align: left">Post-processing &amp; effect chaining</td>
      <td style="text-align: left">3</td>
      <td style="text-align: left">1.5 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/platformer/index.html">Ultimate Platformer</a></strong></td>
      <td style="text-align: left">Procedural game assets &amp; backgrounds</td>
      <td style="text-align: left">19</td>
      <td style="text-align: left">5.3 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/maze3d/index.html">3D Maze Explorer</a></strong></td>
      <td style="text-align: left">Real-time animated 3D environments</td>
      <td style="text-align: left">8</td>
      <td style="text-align: left">1.6 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/marble_cube/index.html">3D Marble Cube</a></strong></td>
      <td style="text-align: left">Dynamic portals &amp; face-swapping logic</td>
      <td style="text-align: left">6</td>
      <td style="text-align: left">1.7 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/marble_roller/index.html">Marble Roller</a></strong></td>
      <td style="text-align: left">Physics-based materials &amp; tiling</td>
      <td style="text-align: left">5</td>
      <td style="text-align: left">1.8 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/flight_sim/index.html">Flight Sim</a></strong></td>
      <td style="text-align: left">Infinite landscapes &amp; day/night cycles</td>
      <td style="text-align: left">5</td>
      <td style="text-align: left">3.3 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/card_roguelike/index.html">Card Roguelike</a></strong></td>
      <td style="text-align: left">Seeded UI frames &amp; ornate artwork</td>
      <td style="text-align: left">19</td>
      <td style="text-align: left">4.2 KB</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><a href="https://tonym128.github.io/texgen/example/solitaire/index.html">Procedural Solitaire</a></strong></td>
      <td style="text-align: left">Realistic paper &amp; felt material textures</td>
      <td style="text-align: left">3</td>
      <td style="text-align: left">1.2 KB</td>
    </tr>
  </tbody>
</table>

<h2 id="try-it-yourself-inline-examples">Try it Yourself: Inline Examples</h2>

<p>You don’t need a heavy engine to start creating. Here are a few ways to talk to TexGen.</p>

<h3 id="1-the-glsl-approach">1. The GLSL Approach</h3>
<p>You can write raw shaders with interactive sliders. Here’s a simple animated plasma:</p>

<div class="language-glsl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">uniform</span> <span class="kt">float</span> <span class="n">u_scale</span><span class="p">;</span> <span class="c1">// @slider 1.0, 10.0, 4.0</span>
<span class="k">uniform</span> <span class="kt">float</span> <span class="n">u_speed</span><span class="p">;</span> <span class="c1">// @slider 0.0, 2.0, 0.5</span>

<span class="kt">void</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="kt">vec2</span> <span class="n">st</span> <span class="o">=</span> <span class="n">vUv</span> <span class="o">*</span> <span class="n">u_scale</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">t</span> <span class="o">=</span> <span class="n">u_time</span> <span class="o">*</span> <span class="n">u_speed</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">n</span> <span class="o">=</span> <span class="n">fbm</span><span class="p">(</span><span class="n">st</span> <span class="o">+</span> <span class="kt">vec2</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="n">t</span> <span class="o">*</span> <span class="mi">0</span><span class="p">.</span><span class="mi">4</span><span class="p">),</span> <span class="n">u_scale</span><span class="p">);</span>
    
    <span class="kt">vec3</span> <span class="n">col1</span> <span class="o">=</span> <span class="kt">vec3</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5</span><span class="p">);</span>
    <span class="kt">vec3</span> <span class="n">col2</span> <span class="o">=</span> <span class="kt">vec3</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mi">8</span><span class="p">);</span>
    <span class="kt">vec3</span> <span class="n">color</span> <span class="o">=</span> <span class="n">mix</span><span class="p">(</span><span class="n">col1</span><span class="p">,</span> <span class="n">col2</span><span class="p">,</span> <span class="n">n</span><span class="p">);</span>
    
    <span class="nb">gl_FragColor</span> <span class="o">=</span> <span class="kt">vec4</span><span class="p">(</span><span class="n">color</span> <span class="o">*</span> <span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="mi">0</span><span class="p">.</span><span class="mi">2</span><span class="p">),</span> <span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-texgen-words-natural-language">2. TexGen Words (Natural Language)</h3>
<p>If you’re not a shader expert, you can use <strong>TexGen Words</strong> to describe your vision. Try these phrases in the <a href="https://tonym128.github.io/texgen/example/word_textures/index.html">Word Textures</a> editor:</p>

<ul>
  <li><strong>Lava Flow:</strong> <code class="language-plaintext highlighter-rouge">lava smoke fire hot</code></li>
  <li><strong>Frozen Tundra:</strong> <code class="language-plaintext highlighter-rouge">icy stone marble rough</code></li>
  <li><strong>Digital Glitch:</strong> <code class="language-plaintext highlighter-rouge">neon digital glitch</code></li>
  <li><strong>Mystic Forest:</strong> <code class="language-plaintext highlighter-rouge">forest leaf grass mist</code></li>
  <li><strong>Plasma Vortex:</strong> <code class="language-plaintext highlighter-rouge">purple plasma vortex glow</code></li>
</ul>

<h2 id="where-to-from-here">Where to from here?</h2>

<p>I’m just getting started with TexGen, and there is plenty more on the roadmap:</p>

<ul>
  <li><strong>AI-Assisted Generation:</strong> Further integrating LLMs to help users write custom shader logic from scratch within the editor.</li>
  <li><strong>Node.js CLI:</strong> A dedicated build tool for pre-baking textures as part of a CI/CD pipeline.</li>
  <li><strong>Three.js / Babylon.js Plugins:</strong> Official wrappers to make procedural texture injection as simple as a single line of code.</li>
</ul>

<p>If you’re a game dev or a web artist looking to save some kilobytes, give <a href="https://github.com/tonym128/texgen">TexGen</a> a spin!</p>]]></content><author><name>tonym128</name></author><summary type="html"><![CDATA[Creating TexGen, a lightweight JavaScript library for generating procedural GLSL textures on the fly.]]></summary></entry></feed>