Every New Game Should Need Less Engine Code Than the Last
I gave my engine one rule, and it's not about performance or features. It's this: every new game should need less engine-level code than the last one. If game #4 makes me reach back into the engine and write new plumbing, the abstraction isn't real yet. If it just imports what's already there and gets to work, it is.
That single rule decides how the whole thing is structured.
Layers, and dependencies only point down
The engine is thirteen small TypeScript packages stacked in layers. The layers are strict: a package may depend on packages below it and never sideways or up.
01 foundation math · geometry · core
02 simulation physics · ecs
03 runtime engine
04 rendering render-core · render-webgl2
05–09 domains world · gameplay · net · tools · scientific
geometry depends on math. physics depends on math and geometry. The engine runtime depends on core and ecs. Rendering depends on math. Nothing below ever reaches up to something above it. Every one of those arrows is a real entry in a package.json, not a diagram I drew hopefully — the dependency graph is the architecture.
Why so strict? Because down-only dependencies are what make a layer reusable in isolation. math doesn't know physics exists, so I can pull math into a totally unrelated project and it drags nothing behind it. A cycle anywhere — even one — and the whole stack collapses into a single blob you can only take or leave whole.
Flat names, grouped folders
The packages live in numbered folders (packages/02-simulation/physics) but they're named flat (@engine/physics). That's deliberate. Imports reference the name, never the folder:
import { World } from "@engine/physics"; // not "../../02-simulation/physics"
So I can reorganize the layers on disk — renumber them, regroup them — and not a single import breaks. The folder structure is for humans reading the repo; the names are the stable contract. Layout and API get to change independently.
The CLI won't hold your hand
You don't install the whole engine. You vendor the packages you want straight into your project:
npx github:Adhirajsingh2507/engine-ecosystem add math geometry physics
The interesting part is what happens when you ask for a package but skip its dependencies. Most tools would quietly pull them in. Mine refuses:
$ npx engine-ecosystem add physics
Cannot add — missing dependencies (they are not auto-added):
@engine/physics needs: @engine/math, @engine/geometry
Add them explicitly, e.g.:
npx engine-ecosystem add physics math geometry
This looks user-hostile for about five seconds, until you realize it's the same down-only rule enforced at install time. Auto-adding dependencies hides the graph — you end up with packages in your tree you never asked for and can't explain. Making you name them keeps you honest about exactly what you're pulling in. A dependency counts as satisfied if it's already vendored or named in the same command; otherwise the command stops and tells you precisely what's missing. No magic, no surprises in your node_modules.
"No empty stubs" keeps the map honest
The roadmap lists far more than thirteen packages — a WebGPU backend, audio, an editor, world streaming. None of them exist as empty folders. The rule is: built code only, no stub packages. A layer appears in the repo when it has real, tested code, and not one commit before.
It's tempting to scaffold the whole target architecture up front — forty empty directories, each with a hopeful TODO. That's how you get a repo that looks finished and does nothing. Deferring a module honestly (a line in the roadmap) beats stubbing it dishonestly (an empty package that lies about being there). The repo grows as it fills, so its size always tells the truth about what actually works.
The lesson
Every decision here is the same decision wearing different clothes: strict down-only layers, folder-independent names, a CLI that won't auto-add, a roadmap with no stubs. They all serve the one rule — that the next game should lean on the engine more and write less.
The way you prove an abstraction is real isn't by admiring it. It's by building the next thing on top and watching how much new code you had to write. If the answer keeps shrinking, the layers are doing their job. If it doesn't, no amount of clever structure will save you — and it's time to go find out which layer is lying.