Devlog · Behind the scenes

How a 2-Person Studio Built a Browser Roguelite with AI

Alley Cats: Cheese Run — menu key art

We're a two-person, AI-native studio, and Alley Cats: Cheese Run is our bullet-heaven roguelite — a Vampire-Survivors-like where you play as a cat, dodge a swarm, and snowball a build. It's free, it runs in your browser on desktop and mobile, and it's built in a way that let two people ship two full stages with roughly twenty enemy types. This is the honest version of how we did it: the architecture, the $0 AI pipeline, and the parts that bit us.

In this devlog

  1. Why a browser game, and why a survivors-like
  2. The single-file architecture
  3. Data-driven content: widen the table, don't fork the code
  4. The $0 AI art & audio pipeline
  5. Verifying a game with no human in the loop
  6. What we'd tell another small team

Why a browser game, and why a survivors-like

The first decision was distribution. A browser game has zero friction — no installs, no launchers, no store gate, no "download 4 GB and reboot." You click a link and you're playing, on the phone in your hand or the laptop on your desk. For a tiny studio without a marketing budget, that link is the funnel. Every extra step between "curious" and "playing" is people you lose.

The second decision was genre. A survivors-like is readable in ten seconds and satisfying almost immediately. Your cat auto-fires — the game aims for you — so the player's whole job is dodging and choosing which power-ups to stack. That's a beautiful fit for the web: no tutorial wall, no twitchy aim on a touchscreen, just move-and-survive. The dopamine loop (level up, pick a card, watch the screen fill with more of your own bullets) hooks fast and reads on any device.

🔗

Friction is the enemy. A browser game's share link is the entire onboarding funnel. Every install, launcher, or account wall between curiosity and play is churn you can measure.

The single-file architecture

Here's the part people don't believe at first: the whole game is one game.js — around 1,700 lines — plus one index.html. Pure Canvas2D. Zero dependencies. No build step, no bundler, no framework, no transpile. You open the file and you're looking at the entire game.

That sounds like a constraint. It's actually a superpower, and specifically a reliability superpower. With no modules there are no importmap or module-path 404s. With no build there's no toolchain to break between you and a running game. And because our host is Linux — which is case-sensitive about filenames while our dev machines are not — the single-file approach sidesteps a whole class of "works locally, 404s in production" bugs that used to cost us afternoons on our bigger 3D project. Deploying is copying two files. Debugging is reading one.

The best architecture for a two-person studio isn't the most elegant one — it's the one with the fewest ways to fail silently at 11 pm before a deploy.

Under the hood, the game runs a fixed-substep 60 Hz simulation loop. Real elapsed time feeds a 1/60 accumulator, so the sim advances in deterministic fixed steps while rendering stays variable. That's what keeps the game real-time and consistent even on a slow laptop dropping frames — the physics don't speed up or slow down with the frame rate. It's also how we get "juice" for free: hitstop and slow-motion scale the sim accumulator, not the render. A satisfying freeze-frame on a big hit is just the sim time-scale dipping for a few milliseconds; the screen keeps drawing at full smoothness.

⚙️

Decouple sim from render. A fixed-substep loop keeps the game deterministic and real-time at any frame rate, and it makes game-feel effects (hitstop, slow-mo) a one-line time-scale tweak instead of a physics rewrite.

Data-driven content: widen the table, don't fork the code

Two people cannot hand-code twenty enemies. So we didn't. Almost everything that is the game — enemies, powers, cat skins, seasons — is a row in a config table, not new code.

New enemies live in an ENEMIES registry where each row carries stats plus a behavior hook: a short string that names how it moves and attacks (chase, dart, pickpocket, charge). Power-ups are rows in an UPGRADES table. Each cat's signature power is another row. Adding content means widening a table, not forking the loop. Want a new enemy? Add a row and, if its behavior is genuinely new, one small hook function everything else already knows how to call.

This is the single biggest reason two people shipped two full stages with about twenty enemy types. The behavior of the game lives in data you can scan on one screen, and the code that drives it stays small and stable. It also makes the game trivially seasonable — a holiday event is a batch of new rows, not a new build.

🗂️

Content is data, not code. When enemies, powers, and skins are table rows with a behavior-hook string, a tiny team can ship a lot of content — and add a seasonal event without touching the game loop.

The $0 AI art & audio pipeline

People assume "AI-native studio" means a stack of API bills. It's the opposite. Our shipping default is procedural-first: procedural art, WebAudio-synthesized sound, and canvas VFX all ship for $0 and mean the game is playable long before any art is "finished." Crucially, the code has sprite-load hooks everywhere — so when AI-generated art is ready, it drops in without a code change. Playable and pretty are decoupled: we ship playable, then upgrade the look on our own schedule.

When we do generate assets, it runs locally. Our ContentGenAI pipeline lives on a single consumer GPU and uses open, license-safe models: Flux for image generation (sprites and key art), ACE-Step for music, and ffmpeg to cut trailers. No API keys, no per-image cost, no licensing landmines. The menu key art and the trailer footage come off the same box that runs the game in the browser.

🔥

The gotcha that cost us hours: the local image model is only stable when kept warm. Cold-restarting it for every image — spinning it up, generating, tearing it down, repeat — kept crashing on VRAM. The fix was boringly simple: start it once and reuse it across the whole batch. Load the weights, generate everything, then release. Batch, don't cycle.

The net effect is a real asset pipeline with a marginal cost of essentially nothing, which is exactly what a two-person studio needs. Procedural now, pretty later — and "later" costs electricity, not dollars.

Verifying a game with no human in the loop

With two people, nobody has time to manually replay the game across a dozen viewports after every change. So the game verifies itself. There's a window.__game debug contract — an add-only object that exposes internal state (level, HP, spawns, menu selection). A headless Playwright harness uses it to drive the game and assert on real state, across desktop, tablet, and mobile viewports, behind a hard zero-console-error gate. If any viewport throws, the change doesn't ship.

The debug contract being add-only matters: tests never break because a field moved, they only ever gain new things to check. And because the game uses that fixed-substep loop, it stays real-time even when the headless renderer runs slow — the tests can drive it with realistic timing instead of racing a variable clock.

The other lesson was to verify visually. Syntax checks and unit tests never catch a level-up card overlapping another card, or a yellow power-up that's invisible against a yellow background. Screenshots do. Some of our most embarrassing bugs — card overlap, color clashes — were caught by a machine looking at a picture, not by any assertion.

📸

Screenshots catch what asserts can't. Overlap, color clashes, off-screen UI — these are visual bugs. A headless harness that captures and diffs screenshots across viewports finds them before players do.

One bonus fell out of this. We built a ?demo=1 attract mode so the harness had something deterministic to drive — the cat plays itself. That same attract mode is the website hero and the source of our captured trailer footage. The live game is the trailer; there's no separate marketing build to keep in sync. You can watch it run inside the Stage 1 build guide, or just play it.

What we'd tell another small team

If you're a tiny team trying to ship on a weekly cadence, here's the compressed version of everything above:

🐾

Original IP is a hard gate. Look-alikes of trademarked characters are unsellable, full stop. Keep the feeling players love and give it your own faces and names — it costs nothing and it's the difference between a portfolio piece and a liability.

The whole strategy compresses to one sentence: minimize the ways your project can fail, and let the machine do the parts a two-person team can't — the content, the art, and the testing.

That's the studio. A single file, a table of rows, a local GPU, and a robot that plays the game every time we change it. If you want to see the output, the best proof is the game itself.

See it for yourself

Alley Cats: Cheese Run is free, runs in your browser, and has global leaderboards. Or read the build guide to learn exactly how the systems above play out on the screen.

Share: 𝕏 Post Reddit Facebook 🔗 Copy link