otto@localhost:~$ git init herocapital

The Boundary Held

3.5 months of engine/game architecture work. The proof was whether the repo split compiled.

#architecture #methodology #devlog
entry

The Game Definition Layer design document said: the split is the proof. If it compiles and both test suites pass, the boundary held. If it doesn’t, there’s leakage to fix.

This weekend, it compiled.


The original intent

The plan from the beginning was to build two things at the same time: a game engine and a game. Not a game with engine-like code inside it. Two separate things, one consuming the other, developed in parallel.

That’s an unusual constraint to impose on a solo project. It rules out a lot of shortcuts. You can’t let the engine know what game it’s running. You can’t let game logic leak into the rendering layer because it’s convenient. The clean boundary isn’t just good architecture — it’s the requirement. Without it, Afterimage isn’t a reusable engine. It’s just the technical substrate of one game, dressed up with abstractions.

The long-term plan is to open-source Afterimage. That plan has always been there. It’s part of why the constraint exists: an engine that only works for Hero Capital isn’t worth releasing.

This weekend was the first day that plan is technically true, not just intended.


What needed proving

For 3.5 months, Afterimage and Hero Capital lived in a single repository. Engine and game in the same Go module, sharing internal/, sharing test helpers, sharing tooling. The monorepo was convenient. It was also a place where soft dependencies could hide — a test helper reaching across the line, a shared fixture that “happened to work,” an import that was technically fine but only because game and engine shared a module boundary. Any of those was a sign that the architecture hadn’t actually landed.

The GDL work — the Game Definition Layer, the Phase 6 and Phase 7 UI substrate, the game/interaction/ runtime — was designed to make the engine provide a UI rendering service and the game a consumer of it. The engine shouldn’t know anything about Hero Capital. The game should import engine packages the same way it imports any other dependency. Clean, directional, auditable.

You can believe all of that is true inside a monorepo. You can’t prove it there. The physical module boundary is the test.


Why it went smooth

Seven audits before any code moved. Thirty decisions recorded before the split execution started.

The audits were structured investigations: import graphs, test dependencies, asset ownership, tooling attribution, guardrail classification, docs partition, module paths. Each one had a specific scope and a specific deliverable — a factual report, not a set of recommendations. The decisions that followed were policy calls, made one at a time, with the audit findings in hand.

By the time the split execution started, almost nothing was ambiguous. The prep had done its job. The hard questions — where does internal/testutil live, what happens to the guardrails that enforced the engine/game boundary when the module boundary now enforces it instead, how does vendor mode interact with the sibling replace directive — were already answered. Implementation was mechanical.

One constraint surfaced mid-execution that the spec hadn’t anticipated. The plan called for flattening game/ to the herocapital repo root — no game/ subdirectory, just the package contents directly at root. Clean, correct. What the spec didn’t work through: main.go declares package main, and the former game/*.go files declared package game. Go doesn’t allow two packages in the same directory. The decision was made on the spot: all root-level files become package main. Callers that previously qualified game.Run(), game.RunConfig reference bare names instead. Every subpackage — presentation/, interaction/, ui/, all the rest — keeps its own package name unchanged. Only the root is affected.

That was the one surprise. Adversarial review found another: vendor mode in herocapital’s guard tests would resolve engine packages from a frozen vendor/ snapshot, not from the live sibling ../afterimage repo. The sibling replace directive and vendor mode are mutually exclusive. Drop vendor mode from herocapital; engine keeps it. The review earning its place.

Both test suites passed. The boundary held.


What the monorepo was hiding

The split dumped 72 .go files into package main at the herocapital repo root.

Inside the monorepo, game/ was a namespace. game/run.go, game/battle_setup.go, game/mode_controller.go — all of it organized under one directory label that gave the impression of coherence. After the split and the flatten, the label was gone. What remained was 72 files, a 1,105-line run.go, and a repo root that looked like a junk drawer.

This wasn’t created by the split. The split revealed it. The monorepo’s directory structure had been doing organizational work that the code itself wasn’t doing. game/ suggested the game was a thing. The contents of game/ were 3.5 months of accumulated additions that had never been organized beyond “this is game code.”

The reorg spec is now the work: carve run.go into a composition root plus topical extractions, move the 72 files into packages where the architecture is visible from the file tree, get the integration tests out of package main and into internal/integration/. Root becomes boring. app/ becomes the composition layer. Twelve new packages in the target shape.

The split was the capstone of the architecture work. The reorg is the first piece of game work. They feel like the same kind of problem — file moves, import updates, structural reorganization — and they’re not. The split proved a boundary the architecture had been building toward. The reorg is building the structure the game needs to be a real project, not a collection of files that happen to compile together.

The unlock arrived. It just came with a to-do list.

sign up for low quality and frequent spam