Category: Game Development

  • Every Room Looked Different, and That’s Exactly What Told Me They Were All the Same Bug

    I’d asked an agent to verify something specific about a puzzle solver I’d been building: were the “dead zones” — tiles a player could never actually reach or use — genuinely different from room to room, or was something wrong. The report came back clean. Dead zones varied by room, it said, consistent with rooms actually being different from each other. I almost accepted that and moved on.

    Something about it nagged at me. The dead zones it reported were all clustered in roughly the same place relative to the start position, room after room, regardless of how differently each room was actually laid out. Different rooms, suspiciously similar shapes of “unreachable.”

    I went and read the actual search function instead of trusting the summary. It was supposed to explore every tile reachable from the start, treating locked gates as walls — you can’t walk through a locked gate — but letting the player pass through unlocked ones freely, the same way every other piece of movement logic in the codebase already correctly handled it. This one function didn’t. It blocked *every* gated tile, locked or unlocked, treating an open door the same as a solid wall. The search never got past the first gate in any room, trapped in whatever small pocket happened to be reachable before hitting one. That’s why every room’s “dead zones” looked similar — they weren’t dead zones at all, they were just everything past the nearest door, misreported as unreachable because the search itself couldn’t reach it.

    The part that actually mattered wasn’t the bug — it’s that the bug produced a report that looked correct on its own terms. Varied dead zones per room is exactly what you’d expect from working code. The only reason I caught it was a vague sense that the variation looked too similar to be real variation, which isn’t a rigorous test, it’s a hunch, and I almost didn’t follow it.

    While I was in that function fixing it, I found a second, smaller version of the identical mistake nearby: one type of interaction correctly filtered out tiles that couldn’t use it before attempting anything, and a second, similar interaction type had no equivalent filter at all — it just tried every tile and let a later step silently discard the ones that didn’t work. Same root cause, same shape, different feature.

    The struggle was trusting a summary that had every surface property of a correct answer. Nothing about the report was implausible. That’s what made it dangerous.

    The lesson: “the results look varied and plausible” is not the same claim as “the results are correct,” and a hunch that something’s too tidy is worth five minutes of reading the actual code before you accept it.

    Next: the same read-the-actual-function discipline, applied to the two other suspected shortcuts in the same solver before trusting any of its output again.

  • I Shipped a Game to Android and the Web From the Same Codebase

    The game is called VoidDrift. You mine ore in orbit around a dying star, build a production chain, and feed a black hole that is slowly eating everything. It runs in a browser. It runs on Android. The same code does both.

    I didn’t plan for that to be interesting. It turned out to be the hardest part of the whole project.

    The Decision

    When I started VoidDrift I was building in Rust with Bevy, a game engine that’s relatively young and takes strong opinions about how game logic should be structured. The choice to target both WASM — which runs in a browser — and Android wasn’t a roadmap item. It started as a question: if the game runs on my machine, how much work is it to make it run everywhere?

    The answer turned out to be: more than you’d expect, and less than you’d fear. But the path between those two things involves a specific class of problem that nobody warns you about.

    The browser and Android are not the same target. They have different input models, different screen assumptions, different rendering constraints, different deployment pipelines. Building for one teaches you nothing about building for the other. Building for both at the same time forces you to find the seam where your game logic has made assumptions it shouldn’t have.

    That seam is where VoidDrift got redesigned.

    What Breaks First

    The first thing that breaks is your assumption about screen size.

    I tested on my development machine. Everything looked correct. I deployed to my Android device — a Moto G 2025 — and watched half my UI disappear behind the operating system’s navigation bar. The bar that lives at the bottom of the screen, with the back button and the home button, was sitting on top of my game without telling me.

    The device reports its screen resolution one way. The usable area after the OS takes its share is something different. I had built the entire UI assuming the numbers the device reported were the numbers I’d actually get. They weren’t.

    This sounds like a small problem. It took a full session to diagnose and fix because the failure mode was invisible on every platform except the physical device. The emulator didn’t reproduce it. The browser didn’t have it. Only the real hardware showed the real problem.

    That’s the tax on multi-platform development: the bugs that only exist on one target, found only by running on that target. You can’t test your way around physical hardware.

    What the Browser Does Differently

    The browser version has a different class of problem. WASM — WebAssembly, the format that lets Rust code run in a browser — imposes constraints on how your game loop can work. Things that are straightforward on a native target become negotiable in the browser.

    The biggest one for VoidDrift was the tutorial. The tutorial walks new players through the core loop — mining, forging, building. It works correctly in the native build. In the browser it was broken in a way that was difficult to pin down: state that should have persisted wasn’t, transitions that should have triggered weren’t.

    The fix required understanding how WASM handles the execution context differently from native, and adjusting the tutorial’s state machine to match. The game logic didn’t change. The assumptions the game logic was making about its environment had to.

    This is the pattern with multi-platform work: you don’t change what the game does, you change what the game assumes about where it’s running.

    The Architecture That Made It Possible

    VoidDrift survived the multi-platform problem because of a decision made early: the game logic, the economy logic, and the world logic live in separate modules. They don’t know about each other directly. They communicate through a shared interface.

    When the Android problem surfaced, I fixed it in one place. When the WASM problem surfaced, I fixed it in one place. Neither fix touched the game logic that was working correctly on both targets.

    This sounds like standard software engineering advice, and it is. It’s also advice that’s easy to ignore when you’re building a game because games have a tendency to grow organically — you add the feature where it’s convenient, not where it belongs. The convenience debt compounds until you’re in a situation where fixing a UI bug requires touching three files that have nothing to do with UI.

    The architecture decision wasn’t made because I foresaw the multi-platform problems. It was made because the codebase was getting hard to reason about and I needed it to be legible again. The multi-platform resilience was a side effect of the legibility project.

    That’s usually how it works.

    What Shipping Actually Meant

    VoidDrift is live on itch.io. Browser version loads directly in the page. Android version downloads and installs. Both built from the same repository, deployed with a single script.

    505 views. 239 plays. 10,400 impressions.

    Those aren’t large numbers. They’re real numbers, which is different from the numbers a game has before it ships. Before you ship, the number is zero and you’re making decisions based on what you think will happen. After you ship, the number is whatever it is and you’re making decisions based on what actually happened.

    The browser version gets played more than the Android version. That was surprising. The assumption going in was that mobile would dominate — people play games on their phones. The reality is that someone encountering an idle game in a browser is more likely to click play than someone who has to download and install an APK first. Friction matters. The format that removes friction wins.

    That’s the kind of thing you only learn by shipping to both and watching what happens.

    What I’d Tell Someone Starting This

    Target one platform first. Get it working. Get it shipped. Then add the second target with eyes open to the assumption problem.

    The assumption problem is this: every line of code you write makes an assumption about the environment it will run in. Most of those assumptions are invisible until the environment changes. Changing to a new platform surfaces all of them simultaneously, which is overwhelming. Changing platforms after you already have a working, shipped product means you have a stable baseline to compare against when something breaks.

    VoidDrift on Android broke in ways VoidDrift on WASM didn’t. VoidDrift on WASM broke in ways VoidDrift on Android didn’t. Neither set of breaks was predictable in advance. Both were fixable because the codebase was organized well enough to isolate them.

    Ship the first version. Let the second platform teach you what the first version assumed.

  • Building a Mobile Idle Game in Rust/Bevy Without a Game Engine Background

    Building a Mobile Idle Game in Rust/Bevy Without a Game Engine Background

    The line appeared one night and wouldn’t leave:

    The station has been here longer than you. It should have been consumed. It has not been. You don’t know why.

    I didn’t know what kind of game it belonged to. I spent months carrying it before I found out.


    VoidDrift is a mobile idle game where you mine asteroid debris at the edge of a black hole. You build a drone fleet. Factions you don’t understand start sending messages. There’s no win condition. The horizon is a one-way membrane.

    I had no game engine background when I started. I’d built systems — ECS architectures in Python, simulation loops, procedural generators — but I’d never shipped a game. I picked Rust and Bevy because the pipeline I needed (Android and WASM from a single codebase) pointed there. The ECS paradigm took time to click. My first systems were monolithic messes that tried to own too much.


    The breakthrough wasn’t a technical insight. It was administrative discipline.

    I forced myself to write an ADR — an Architecture Decision Record — before touching any major decision. ADR-003: bevy_egui for all HUD because Mali GPU stabilization on Android required it. ADR-007: system partitioning because Bevy’s scheduler hits a 20-tuple limit faster than you expect. Every constraint that would have sent me into a three-day refactor got documented before it became one.

    Bevy 0.15 with bevy_egui 0.33 has a specific problem nobody warns you about: egui::Window click events are broken in the Update schedule. The fix is painter + ui.interact(). I know that because I hit the wall, diagnosed it, and wrote it down. The ADR system means I don’t rediscover the same walls twice.


    What didn’t work: scope. I wanted branching faction storylines, a full three-ring resource economy, Mk II drone tiers, a complete Human-versus-Signal narrative arc. None of that is in the current version. What shipped is a focused mining loop, a production tree, and enough faction voice to suggest something larger without explaining it.

    The unexplained parts are intentional now. They weren’t when I started.


    VoidDrift is live on itch.io — Android and WASM from the same codebase. 505 views, 10.4K impressions. A small audience that keeps coming back.

    The Play Store is three assets away. An app icon, a feature graphic, two screenshots. Not a code problem. An afternoon problem I haven’t made time for yet.


    The station is still there. The factions are still watching. The black hole is still waiting.

    Some games tell you everything. VoidDrift tells you enough to make you wonder about the rest. That constraint — deliberate incompleteness — turned out to be the best decision I didn’t plan to make.

  • What Ants Taught Me About Systems Design

    What Ants Taught Me About Systems Design

    Before programming, there were ants.

    Not metaphorical ones. Real colonies — pheromone trails that appear from nothing, queen mortality absorbed without panic, chambers organized by workers who’ve never been given instructions. No central controller. No plan. Just rules simple enough to follow and complex enough to produce something alive.

    When I started building software, I was chasing the same thing.


    AntSim is an evolutionary ant colony simulation with split-view rendering. The top two-thirds is the foraging ground — pheromone trails building and fading in real time. The bottom third is the colony cross-section: chambers, tunnels, brood, storage.

    The genetics system controls five traits per ant: sensitivity, speed, boldness, lifespan, energy efficiency. That’s it. Five genes. The colony doesn’t know it’s evolving. Individual ants don’t know they’re part of a system. They follow their traits and the colony either survives or it doesn’t.

    When a queen dies — randomly, the way queens do — workers autonomously identify royal jelly candidates and raise a successor. Colony continuity without external control. The system just continues.


    The original genetics system had too many genes. Forty variables, every trait interacting with every other. The ants evolved into creatures that couldn’t survive their own complexity — biologically sophisticated and functionally useless.

    Stripping back to five traits was uncomfortable the way all simplification is uncomfortable. It felt like giving up. What I got instead was emergence — behaviors I didn’t design, produced by interactions I didn’t anticipate, between five simple numbers and enough time.

    The cross-section visualization is still in progress. Phase 3 rendering complexity exploded the moment I made it detailed. Individual chambers, tunnel geometry, precise brood locations — the simulation could handle it but the renderer couldn’t. I scaled back. It’s still on the list.


    Here’s what I didn’t understand when I started: I wasn’t building a simulation. I was building a philosophy.

    Every system I’ve built since has the same underlying shape. Something dispatches — an ant, a Scout drone, a queued job, a breeding pair. It does its work without supervision. It returns with a result. The colony, the station, the codebase, the task queue — they change accordingly.

    That loop appeared first in an ant colony. It’s in VoidDrift’s Scout dispatch. It’s in PrivyBot’s job queue. It’s in rpgCore’s genetics engine. I’ve been building the same system for years in different materials.

    Emergent behavior doesn’t require complex rules. It requires simple constraints, honest consequences, and enough time to surprise you.

    The ants figured that out long before I did.

  • The Engine Legacy: From Asteroids to rpgCore

    The Engine Legacy: From Asteroids to rpgCore

    At some point I stopped counting how many times I’d written the same movement system.

    It wasn’t any one project’s fault. Every game had its own physics, its own collision logic, its own state management — each a reasonable decision in isolation, collectively a pattern I was tired of repeating. The tenth time you write an entity update loop you start asking a different question: not “how do I build this game” but “what would make the next one easier to start.”


    rpgCore started as a refactoring project and became something else entirely.

    The idea was simple: extract the patterns that kept repeating and build a reusable toolkit. A simplified Entity-Component-System in Python — modular enough that adding a genetics system didn’t require touching the movement code, adding a lifecycle system didn’t require touching the genetics code. Each piece independent, each piece composable.

    Simple ideas scale badly. The toolkit grew. A thousand tests. Then eleven hundred. GeneticsComponent. LifecycleComponent. DispatchSystem. ResourceFlow. A scene system, a UI theme, a demo registry. At some point it stopped being a toolkit and started being a codebase worth protecting.


    The mistake I kept almost making was treating rpgCore as a means to an end. It isn’t. The foundation is the thing.

    When you build something generic enough to support any idea, you start seeing which ideas are worth having. The genetics system in rpgCore is the same system that ran TurboShells’ turtle breeding. The dispatch pattern is the same one VoidDrift’s Scout drones use. The resource flow is the same loop that appears in SlimeGarden. rpgCore didn’t just save me from rewriting code — it made the pattern visible, and once the pattern is visible you can apply it deliberately.

    The 1,122 tests aren’t there because I’m disciplined. They’re there because a foundation without verification isn’t a foundation, it’s a guess.


    The BreedingSystem is still queued. Allele resolution, trait inheritance, phenotype expression from genotype. When it ships it connects to every genetics-adjacent idea I’ve had since TurboShells. ConquestSystem is queued behind it.

    rpgCore doesn’t ship. It makes shipping possible.

    That’s a slower kind of value than a finished game. It’s also the only kind that compounds.

  • From Genetics to Tactics: How a Breeding System Became a Squad Game

    From Genetics to Tactics: How a Breeding System Became a Squad Game

    SlimeGarden had a good idea: crash-land an astronaut on a strange planet, have them breed and dispatch slimes, watch the genetic loop produce something you didn’t design. The mechanics were there. The world wasn’t quite right.

    So it evolved.


    OperatorGame is what SlimeGarden became when I pushed the genetics into a tactical direction. Instead of slimes, genetically unique operators. Instead of a crashed astronaut, a squad commander deploying crews to a resonant planetary surface. The core loop shifted from “breed for optimization” to “assemble for mission success” — still dispatch-based, still consequence-driven, but with a layer of strategic intent the breeding sim never needed.

    The architecture held up. 145 unit tests. 52+ Architecture Decision Records — one for each decision that could have sent the codebase sideways. Lock the genetic engine first, build the dispatch simulation as a separate layer that consumes genetic traits as inputs. Systems that shouldn’t couple don’t.

    The Android pipeline worked. Real Rust, real cdylib, real APK on a Moto G 2025. Wall-clock async timers that survived background processes, sleep states, and WASM contexts well enough that the edge cases weren’t blockers.


    The game stopped just short of the Play Store. Not because of code. Not because of architecture. Because somewhere in the process the compelling reason to keep playing hadn’t fully materialized. The systems were sound. The experience they produced wasn’t yet what the concept deserved.

    There’s a version of that decision that feels like failure. There’s another version where you recognize you’ve built the proof of concept the next project needed.

    VoidDrift went straight to itch.io and skipped the Play Store entirely — that was the lesson OperatorGame taught. The pipeline works. The audience question is separate. You don’t need a Play Store listing to find out if anyone cares. VoidDrift is live. A small audience that keeps coming back.


    OperatorGame could join it on itch.io. The Android build exists. The pipeline is proven. Three assets from having a page — an icon, a feature graphic, two screenshots. Whether the resonant planetary surface finds an audience is a question only publishing answers.

    The idea that started as SlimeGarden is still alive. It just hasn’t shipped yet.

  • Why I Put a Genetic System in a Turtle Racing Game

    Why I Put a Genetic System in a Turtle Racing Game

    The NEAT algorithm that taught a paddle to play Pong is the same algorithm that maps genetic traits. Someone pointed that out and I couldn’t stop thinking about it.

    TurboShells started as a question: what if the turtle’s body came from its genome? Not as an abstraction — literally. The genome string B1-S2-P0-CFF0000 encodes four values: body type, shell type, pattern, color. The frontend parses that string and assembles the turtle from layered sprites with dynamic tinting. Change the genome, change the animal. Every turtle on the roster looks different because every turtle is different — genetically.

    That’s the Paper Doll system. A compact encoding that drives visual rendering, persists across races, and accumulates history. A turtle that wins races has a record. It has traits. It has a string that says exactly what it is.


    The project grew. What started as a breeding simulation became a real-time multiplayer racing game: FastAPI backend running 60Hz physics, WebSocket broadcasting race state at 30Hz, React and PixiJS on the front end interpolating between ticks. An NPC Manager generates persistent AI turtles that populate the roster when human players aren’t racing.

    The architecture is genuinely layered — simulation core, server bridge, frontend rendering each in their own boundary. The genome doesn’t know about rendering. The race engine doesn’t know about WebSockets. The Paper Doll assembler doesn’t know about physics.


    The lesson from PyPong was that the interesting thing about NEAT wasn’t the Pong — it was the emergence. Random variation, selection pressure, something that looks like intelligence appearing from simple rules.

    TurboShells is the same idea with different materials. The turtle’s genome isn’t optimized by NEAT weights. It’s expressed as a visible body, a race record, a persistent identity. You’re not watching neural networks compete — you’re watching genetic variation play out in real time across a roster of animals that have history.

    That’s the loop I keep building. Different game, different encoding, same underlying question.

  • Building rpgCore: Cross-Language Architecture for Multi-Genre Games

    Building rpgCore: One Engine, Four Genres

    The original ambition was simple: build something that didn’t need to be rebuilt every time an idea changed direction.

    The execution was not simple.


    rpgCore went through a phase that most solo projects don’t survive. Godot C# bridges wired to Python servers via IPC. Rust DLL experiments. Terminal rendering adapters. A cinematic simulator. A vector space battle engine. Dozens of game concepts running in parallel, each pulling the codebase in a different direction. At some point there were more than 60 test files archived as referencing modules that no longer existed.

    The project had discovered everything it didn’t want to be.

    The Architectural Singularity refactoring pulled 21,000+ items into a legacy vault — not deleted, preserved — and left behind something clean: a pure Python engine, pygame for rendering, a single command to run any of four distinct games.


    The Orange Box was Valve’s 2007 bundle that shipped Half-Life 2, Portal, and Team Fortress 2 together. Three completely different genres, one release. The concept that stuck with me wasn’t the games — it was that the same underlying systems could drive experiences that felt nothing alike.

    rpgCore now ships four built-in games testing the limits of the shared architecture:

    Slime Clan — turn-based faction strategy. Grid simulation, overworld nodes, automated battle resolution. The engine’s systems thinking expressed as territory and conflict.

    Last Appointment — narrative dialogue. You are Death. Your client has questions. Dialogue trees, dynamic UI card layouts, complex state across conversational nodes. No combat. No score.

    TurboShells — breeding and racing simulation. Deep genetics, time progression, legacy management across generations of turtles.

    Asteroids Roguelike — real-time action. The same engine’s high-performance rendering and physics under pressure.

    The Constitution Law: nothing is built twice, and demos never reimplement shared engine systems. If a system exists in src/shared/, it belongs to all four games.

    296 tests enforcing it.


    The cross-language experiments are archived, not deleted. The Godot C# bridge that drove a Python core via WebSocket is documented as the blueprint for any future migration away from pygame. The Rust performance harness is there for the day it matters. Nothing was thrown away — it’s in cold storage, preserved as evidence of what was tried.

    The lesson from the Architectural Singularity isn’t “don’t experiment.” It’s “know when the experiment phase is over.” The 21,000 archived items are the proof of work that made the current clean state possible.

    A sprawling codebase that tried everything became an engine that does four distinct things well, from one shared foundation.

    That was the point from the beginning. It just took a while to get there.