Hello, time for a confession: I am building a single-player strategy game in TypeScript, running in a browser engine, and every gamedev forum I have ever read says this is wrong. The standard advice is Unity or Godot, and the standard advice exists for good reasons. I spent about three weeks second-guessing myself before committing, so this first post is the argument I eventually won against my own doubts. If the project collapses in six months under garbage collection pauses, you will at least be able to point at this post and laugh with full context.
This is mostly not a game, it is an interface with a game inside
First, what Mutarch is: you run a colony of spliceable mutant creatures, send them on expeditions across a procedurally generated overworld, and watch them fight auto-battler combats against rival clans. One person builds all of it: design, code, art pipeline, balancing, bugs.
Now the engine question, and let me steelman the defaults properly before rejecting them.
Unity is the safe choice. It is battle-tested at every scale, the asset store can sell you half a game, and every problem I will ever hit has a forum thread from 2019 with an accepted answer. Godot is the choice I actually wanted to like. It is open source, its 2D tooling is genuinely good, and the scene system maps nicely onto how I think about game objects. I spent a weekend prototyping the splice bench in it, which is the screen where you drag genes between creatures, and the prototype worked.
But the weekend also surfaced the real constraint. The splice bench needs scrollable lists, drag-and-drop with keyboard fallback, tooltips that follow focus, and text that reflows when a creature name is long. That is one screen. The design doc lists a roster, a gear bay, an atlas of everything you have encountered, a region map, an evolution tree, and more screens I have not started. Mutarch is roughly 80 percent interface by surface area. The honest framing is not “a game with some menus” but “a very large form application with a real-time arena embedded in it.”
Here is the edge case that killed the prototype for me: what happens to a tooltip when the German translation of a skill description is 40 percent longer than the English one and the panel it sits in is anchored to a scrolling list? In web tech, that is Tuesday. The text layout engine, the flexbox model, the accessibility tree, the inspector that lets me poke at all of it live: that is 25 years of accumulated tooling for exactly this problem. In a game engine, UI like this is something you rebuild, widget by widget, and the rebuilt version is always a little worse than the thing every browser ships for free.
Unity and Godot are not bad at games. They are bad at the part of my game that there is the most of. So the choice inverted: instead of a game engine where I fight the UI, a UI platform where I bring my own game.

The stack, one floor at a time
Each layer earned its place with one sentence, so here are the sentences. Version numbers are from package.json as of this week.
- TypeScript 5.8, strict mode. Refactoring a solo project without types is self-harm; the compiler is the only colleague who reviews my code.
- React 19.1 for every screen that is made of panels, lists, and text. I do not want to be clever about UI state; I want to be boring about it.
- Pixi.js 8.10 (plus
@pixi/react8.0 as the bridge) for the canvas scenes: the combat arena and the colony view. WebGL sprites where sprites belong, DOM everywhere else. - Zustand 5 for application state. It is a small store with no ceremony, which matters when one person maintains every subscription.
- Miniplex 2 as a tiny ECS for combat entities. Combat wants archetype queries over hundreds of units; React wants none of that, so they never meet.
- Vite 7 for development (dev server pinned to port 1420), Vitest 4 for tests, Storybook 10 for building UI components in isolation before they touch the game.
- i18next from day one, because retrofitting localization is the kind of debt that compounds.
- pnpm, pinned to 10.34.1, on Node 22 or newer (
.nvmrccurrently says 26.3.0).
The number I am most proud of: 15 production dependencies, total. Every one of them is something I would not want to write myself. The temptation in the npm ecosystem is to install a solution for every inconvenience, and the defense is a rule: a dependency must replace more code than it adds surface.
The wall that everything leans on
The decision that matters more than any library choice is a folder structure:
src/
game/ pure TypeScript simulation, zero React imports
ui/ React screens and components
pixi/ canvas rendering
src/game/ does not know the browser exists. No React, no DOM, no Pixi, no timers tied to frames. It is functions over data: the world ticks, creatures fight, loot drops, and all of it runs the same in a Vitest process as it does behind the UI. I verify the rule mechanically; a grep for React imports inside src/game/ returns zero results, and I intend to keep it at zero with lint tooling rather than discipline, because discipline does not survive 1 a.m. commits.
The arrows only point one way. UI and rendering read from the simulation and send it commands. The simulation never reaches back. This is the load-bearing wall of the whole project, and I am flagging it now because every future post stands on it: testing, determinism, replays, performance work, all of it depends on the sim being a sealed box. If I ever write “and then I imported a React hook into the combat resolver,” stage an intervention.

A Rust shell instead of a bundled browser
“Browser engine” usually means Electron, and Electron means shipping a complete copy of Chromium with your game. Instead, the desktop build uses Tauri 2 (2.11.2 in src-tauri/Cargo.toml): a small Rust shell that opens a window backed by WebView2, the browser engine Windows already ships and keeps updated. The Rust side is nearly empty right now, a window plus the filesystem plugin (tauri-plugin-fs 2.5.1) for save files, and I would like it to stay nearly empty.
I have not produced a release build worth measuring yet, so I will not quote my own binary sizes until I have them. The platform-level facts are enough for the decision: Electron apps carry the Chromium runtime, which puts installers well north of 100 MB before any game content, while a minimal Tauri app builds to a few megabytes because the heavy part is already on the user’s machine. For a 2D pixel-art game that people should be able to run on anything, shipping a private copy of a browser to render tooltips felt absurd. When there is a real build, I will publish the measured numbers here.

What I do not know yet
Honesty section. The doubts are not resolved, they are deferred:
- Garbage collection. The simulation allocates, JavaScript collects, and a GC pause in the middle of a combat frame is a stutter I cannot schedule. I do not know yet how bad this gets at scale.
- One thread. The sim and the UI currently share a single JavaScript thread. Workers exist, but the moment state crosses a worker boundary, the sealed-box architecture gets complicated.
- The “will it scale” voice. Hundreds of units, a whole overworld ticking, thousands of tooltips: nothing I have built so far proves any of it holds up. The forums say it will not. I think the forums are wrong about a 2D game in 2026, but “I think” is not a measurement, and finding out is going to cost me some painful evenings later in this series.
As always, let me know what you think.