A lot of games built quickly feel built quickly. We wanted to work out why Kindlefall mostly doesn't, and it isn't one thing. It's a handful of habits, most of which were written down early and then followed every time. Here they are, for the next game.
1. Write the memory down
An AI session doesn't remember yesterday. AGENTS.md does. It starts with a rule ("every change you make gets a line in the Changelog, and any new knowledge goes into the relevant section"), then the run instructions, a map of every source file, the performance budgets, the rendering gotchas, the audio design notes, and a changelog entry for every build. Every session, every agent, every specialist team read it first.
The part that paid off most was the gotchas section: things we learned the hard way, written so nobody learns them twice. Chrome caching ES modules. Metal returning NaN from pow(0, y). Never resizing the renderer after rendering in the same frame. Changing the number of lights recompiles every shader. When a later agent hit one of these, the answer was already in the file it had just read.
2. Put numbers on "feel"
Early on the AI wrote docs/PLAYBOOK.md: the house standards for game feel, with the real numbers from the code. Every event that matters belongs to a tier, and gets all the channels in its tier:
| Tier | Example | Hit-stop | Shake | Rumble |
|---|---|---|---|---|
| Light | Sword hit, enemy death | 0.05 s | 0.18–0.20 | 0.25, 90 ms |
| Heavy | Combo finisher, Emberbolt blast | 0.08–0.1 s | 0.4 | 0.6, 160–180 ms |
| Massive | Boss slam | 0.08 s | 0.8 | 1.0, 300 ms |
| Cinematic | Boss intro, boss death | slow-motion instead | 0.6–1.0 | 1.0 |
Plus flash light, particles, sound and HUD feedback per tier. "Missing channels are what make a new feature feel cheap." That one sentence is why a boon added overnight by an agent that had never held a controller is still expected to land with its own flash, ring and sound: the recipe for a new boon says so.
The same document sets the rules that make fights readable. Telegraph everything that can hurt the player, at least 0.45 seconds ahead: a body flash and a sound, a ground decal that fills up, or an aim line. A colour language: orange and gold are yours, red is danger, violet is enemy magic, cyan is souls, green is healing, cold blue is the Hollow. And brightness discipline, because big additive shapes blow out through bloom into a white mess. "Always look at a screenshot of the effect at its busiest moment."
3. Keep the loop short and honest
The single most important thing was how fast a note became a build. Play in the evening, write a few lines, and play the result the same night. One playtest day shipped seven builds. That only works if both sides know exactly what's being tested, which is why the build stamp is on the title screen, why stale code shows a red banner, and why every build gets a changelog entry in plain words.
The other half of honest is admitting what the tests can't see. Every hand-back ends with a list of things for a person to judge. No agent has ever heard the game; there's a line in AGENTS.md that just says sound quality has to be judged by a person.
4. Let a dumb bot be the referee
The test bot is ninety lines and can't beat a shield wall. It doesn't need to. It needs to play every floor end to end, with every hero, and scream when something is wrong: a console error, a boss in an invalid state, a room that never clears, a frame over budget. With that in place, agents could prove their own work, and the orchestrator could run the whole battery before every ship. The Sprint #2 brief still says it: run the tests, and "look at your screenshots before reporting".
And never take "done" on faith. The orchestrator sent work back twice because the screenshot didn't match the report.
5. Make it safe to work in parallel
The overnight sprint worked because, before any agent was spawned, the codebase got seams: a registry, hooks, content modules, a written contract. Teams owned slices (a floor, the boons, the heroes) and kept them across rounds, and only the integrator touched the core or shipped. "If two agents would need the same file, add a hook or registry instead." Next time, that comes first, before the first feature.
6. Use constraints as a style
Some of the best-looking things in the game came from rules that sounded limiting:
- CC0 or our own, nothing that needs attribution. So every boon icon is a hand-built SVG in one house style, and there's no limit on making more.
- No recorded music or effects we'd have to license. So the whole soundtrack and every sound effect is synthesized by Python, with a few CC0 Kenney recordings layered underneath for texture. Each floor has its own key, tempo and instruments: Floor III is G minor at 72 bpm with a pipe organ, a music box, a funeral bell and a lone cello.
- KayKit or procedural. Outside models only "if they look like what we already use". So Floor IV's creatures became procedural meshes with a molten shader, and the playtest liked them so much they became the floor's identity.
- One idea per floor. Mixed floor tiles looked like a patchwork; an all-violet floor looked muddy. Both were tried and reverted, and both are written down.
III · cold fire
VII · black glass7. Treat performance as a feature
The development MacBook ran hot early on, so the game caps itself at 60 fps by default (the Mac display runs at 120 Hz and uncapped meant double the GPU work), renders High quality at 1.5x instead of full Retina, and draws menus at 30 fps. The whole dungeon is about 10 to 20 draw calls of merged chunks; the bot's test runs fail any fight over 90.
Two fixes every project should know about. A "performance hit" turned out to be shader recompiles: three.js frees a compiled program when the last material using it is disposed, so every effect that came and went meant a fresh compile and a 4 to 30 ms hitch every few seconds of combat. Pinning programs for the session fixed it. And the first-visit stutter on every floor, up to 74 ms, came from warming up shaders for the canvas when the game draws into an HDR target; warming the right target, and pre-rendering each floor once on the title, brought the worst frame down to 9.6 ms. Memory got the same care: music loads per floor (it was about 400 MB decoded at once), and the sound-effect sprite decodes mono at 32 kHz (325 MB down to 110).
8. Debug the state machine, not the symptom
The first real bug was "the boss gets stuck". Pathing tests couldn't reproduce it, because it wasn't pathing: the Bone Tyrant's attack picker could return undefined when you were far away twice in a row, and he'd sit in an unknown state forever. The lesson in the docs: "when a bug report can't be reproduced by pathing tests, look for state machines that can enter an invalid state." Every state machine in the game now has a default: that falls back to a safe state, and the boss refuses to begin an attack it doesn't know.
9. Let the player own the taste
Looking back over the changelog, the decisions that most define how Kindlefall feels were small calls made while playing: hide boon rarity, keep the two-press confirm, slow the opening down, earn ripostes instead of mashing them, end on a single word instead of a credits roll, keep the long roar for bosses, keep the glare down. None of those are things a test can tell you. The AI's job was to make each one real by the evening. The playtester's job was to notice.
What we'd change
From the team playbook, written after the overnight sprint and the playtest rounds:
- Put the import checker and per-run test ports in from minute one.
- Give every team a brightness and glow budget up front.
- Agree on asset and sound names in the contract.
- Plan the difficulty curve across floors before building them in parallel, because no single team owns it.
And one of our own: keep old builds runnable. Every "before" picture in these posts was made by pulling an old commit out of git, serving it, and letting the same bot play it.