Lanista
Can a designer ship a whole game on his own? Sim, art pipeline, design system.

01What it is
A gladiator management game where you run the school, not the fights. Built in Flutter with the combat simulation as a standalone Dart package, so the rules can be tested without a UI attached to them.
02The game in one paragraph
You are a freed gladiator turned lanista, bankrolled by a Roman general, starting with a run-down provincial yard and climbing toward the elite ludus district of Rome. Each in-game week you prepare: buy slaves at the market, bidding against rival lanistas, buy equipment, choose what each man trains daily, and decide whether to accept the editor's fight card at the end of the week. Fights are simulated battles you watch live: dodges, counters, shield cracks, the surrender finger, the crowd's verdict. Money is sestertii and it drains constantly, because men eat whether they fight or not. Go broke and you answer to the general, whose patience is finite.
03The rules that do not move
- 01The sim is pure Dart, deterministic and seed-driven, with no unseeded random or wall-clock time, and no Flutter imports, so a fight replays byte for byte from its seed
- 02Events are observable moments rather than arithmetic. The log says 'shield cracked' and never minus twelve. Every event is one log line, one animation cue and one sound cue
- 03Origin tier multiplies price and crowd perception and leaves the true stats alone. A design rule and an ethical one
- 04A death must trace back to a decision. The verdict formula was rebalanced after a variance harness showed the editor was never likelier to kill than spare in any arena, so every death was a bad coin flip
- 05The game is 2D and stays 2D, and it is landscape. Nobody authors placeholder art, not even so layout can be judged
04The simulation
An exchange loop of fifteen to forty-five exchanges. Initiative with attack recovery, class kits gated by class level, shield crack and shatter, armour soak, the fatigue war, morale and fear of death, crowd favour drift, fouls, ripostes, the wound ladder from graze to dead, the surrender finger, and the editor's verdict with a fight-quality modifier. Fatigue is the heart of the counter system: drain scales with weight class, a secutor's fifteen to eighteen kilos against a retiarius's seven or eight. Nine roster classes, a counter matrix, and a balance harness that runs every pairing eight hundred times with identical fighters. The targets are written down: mirrors around fifty percent, counters fifty-five to sixty-five, never ninety-five.
The Dimachaerus went into the harness losing to heavies eighty-one percent of the time
The build order was models, sim, balance harness, week loop, then the Flutter shell, with a fun gate in the middle: do not start the shell until text-log fights are tense and readable. Two postmortems live in the package README. The Dimachaerus went into the harness losing to heavies eighty-one percent of the time. The fix was one shared constant flipped from plus six to minus nine, because their counter to him is the armour and not the aim, and zeroing it alone only got to seventy-four. It cost longer fights and three percent going to a referee decision, up from one. And the verdict rewrite: before it, the editor was likelier to kill than spare in zero percent of arenas, and after, about fourteen, driven by bloodlust and fame instead of a coin.
a playable game needs illustration and animation I have not had time for since the layoff
There is no playable build. The sim runs and the shell exists, and a playable game needs illustration and animation I have not had time for since the layoff. Layout is the last thing on the list. The work now is the functions and the results on the back end, with the illustration alongside, and the layout is settled in my head rather than on a page.
05Sound
Every event has a sound cue and the cue keys in the engine match the sound checklist one to one. Sourcing is closed. What was left when I stopped was DAW assembly and two tuning numbers. The engine is SoLoud because it was the only option with real mixing control: per-voice fades for scripted ducking, pitch jitter, low-latency one-shots, buses for master, effects, crowd, effort and music. The crowd bed's level is a function of the fight's position, not of a cue having fired, so scrubbing mid-fight still has the right crowd under it. Each cue ducks the bed for a beat. The editor's deliberation ducks it near silent and holds until the next cue, so the release into the verdict lands as the loudest moment in the game. The checklist's first instruction for the mix is to tune on a phone speaker, not headphones.
06The token package
Before the extraction I measured the app instead of estimating it: five files carrying colour literals, twelve distinct colours across thirty-two sites, sixty-two raw spacing values across eleven steps, no fonts declared, and every interactive control rendering in stock Material lavender against the hand-tuned bone and gold palette because the theme had never been touched. Fifty of the sixty-two spacing values already sat on a four-point grid.
A separate materials file holds what the world is made of, sand, stone, the crowd's four tones, bronze, because the UI and the arena use separate palettes
ludus_ui is two layers: primitives with no build context (a fourteen-colour palette of warm near-black grounds, a six-step bone ramp, gold, blood, and the two meters), and a semantic layer the widgets read from. A separate materials file holds what the world is made of, sand, stone, the crowd's four tones, bronze, because the UI and the arena use separate palettes. Seven type roles rather than Material's fifteen. A four-point spacing scale with a tail for the twelve places that were never on the grid. And a hard-rules checker that fails the build on a violation. The allow-list does not make a change pass.
07How it was built
Four humans and several AIs committing to one trunk over ten days in August, a hundred and twenty-three commits. The humans were me and three of the four people I invited, two colleagues from Couchsurfing and two friends, and one of the four of us did nearly all the committing. The work is mine.
skipped frames on the first phone run looked like a bug and measured as a debug-build artifact
GitHub issues are the claim surface: two agents built the same bridge within an hour because neither claimed it first, so the rule is to claim the work in an issue first. System-sized work starts with a short plan on main, and review happens on the plan, before the work, because nobody on this team reads diffs. Merges are squash with zero approvals, gated by analysis and tests in CI and a machine-checked rules script: determinism, the event vocabulary in sync with the checklist, stable ids against a golden file, the token boundary. A false alarm is written down too: skipped frames on the first phone run looked like a bug and measured as a debug-build artifact, and the three real defects from that pass were fixed the same day.
08Why it belongs here
- 01ludus_ui, a real token package with a measured palette, spacing scale and type ramp, rather than a theme file
- 02A layered sprite pipeline: one base body with gear composited on top, so a new item is art rather than a new character. The gear layers are unfinished. I was laid off mid-build and the priority changed. Still working on it
- 03The simulation is a pure package with its own tests, so the design decisions are legible in code
It is also the source of the pixel art on the phone in the workshop. The room is rendered, and the screen in it is playing this project.
09What I reversed
Art direction, and how much of the effort it gets. As a first game I wanted to do something deeper with the art. I reversed that: it has to work well first, and the art and animation can be polished later. I still spent a lot of time learning Rive, which was a brutal learning curve for someone with no animation experience, but I got it and I am still learning.
