Posted August 19, 2026 by DonutDev
#gamedev #idle #ai
I'm a developer who works by vibecoding — I describe what I want, let AI write most of the code, and spend my own attention on the decisions rather than the typing. Game22 is where I test whether that actually holds up on something real.
Not a to-do app. An idle ARPG with build depth: a stats engine with tagged modifiers, a deterministic simulation core, offline progress replayed by the real combat code, procedural graphics with no external assets, three languages. The kind of project where a wrong decision doesn't crash — it quietly ruins the balance three weeks later.
So the interesting question was never "can AI write this code". It was: can you keep a project like this honest when you didn't type most of it?
The answer I've landed on is: only if you refuse to trust anything you haven't measured. Almost every real defect below looked like "hmm, that build feels weak" and turned out to be a one-line structural mistake. Not one of them was found by reading code.
That's what this devlog is. Not a diary of features shipped — a list of things that turned out not to be what they looked like.
The first thing it surfaced looked like "this build is underpowered" and was structural: character life grew linearly against exponential monster damage. Those curves cross around level 40 — the classic way to break an ARPG. 1,126 deaths in two simulated hours.
The same run hid a second one: exponential mana cost against linear mana. Expensive spells became unplayable around level 20. In the report it read as "Fireball does no damage", when damage had nothing to do with it.
Each of those is about one line of code. You cannot find them by looking.
The balance milestone produced a whole series, and every one was invisible in the source:
physicalResist never reached combat. The stat rolled on affixes, was granted by a tree branch — and got dropped when the runtime was assembled. Every build played with 0% against physical.
Separately, the "Overwhelm" keystone turned out to be a trap rather than a choice: the same build reached T25 with it and T45 with "Retaliation" while dealing three times less damage. Attack speed in the endgame buys leech and ignites, not just damage — the keystone traded something useful for something that wasn't.
English and Spanish sat at 50%, and that wasn't "half done" — it was worse. The translated half was the UI and screens; the untranslated half was abilities, affixes, tree nodes and monsters. Which is exactly the content that appears in lists, side by side: players saw "Frost spike" sitting directly above "Огненный шар" in the same column.
Hence the rule: the unit of translation is a key group, not a key. An empty string is obvious at a glance. Mixed languages look like finished work and survive until someone complains.
Four bugs weren't caught by anything — not the build, not the coverage report, because the string is present and the type checks out:
◆ БОСС ◆ was drawn on the canvas as a literal and stayed Russian in every language;
'12.0 ч', so the time unit froze at whatever language was active on load and sat there in the middle of an English UI;
You don't fix that class of bug by proofreading. You fix it by making Cyrillic string literals in the presentation layers fail a test.
The UI moved from cold blue-grey to warm charcoal. That move exposed something nobody had planned for: the hero silhouette was painted with the UI accent color. The accent became brass — and the player blended into the brown monsters.
The first fix gave the hero blue steel. That landed 45 RGB units from the magic rarity color, and in a pack of blue monsters the player got lost again. The final answer is a turquoise no rarity uses, 95 units from its nearest neighbour.
Which produced my favourite test in the project: it checks distinguishability, not inequality. The first version of that test compared strings and would have passed both bugs.
Found on a release screenshot: all forty items in the bag looked identical. The icons weren't the problem. addToInventory evicted the weakest item by comparing scores across slots — while the docstring on the scoring function says, in as many words, that its values are only comparable within a slot.
The score is based on average weapon damage, which grows exponentially with item level; every other slot uses a flat constant. By the endgame that's 77,686 against 40. Weapons evicted everything. Measured result: forty weapons out of forty.
The function was being used outside its own contract, and the contract was written directly above it.
Too many items were dropping — close to 300,000 over 120 hours. The obvious move: cut the base drop chance. I cut it 45%.
Measured across three seeds, campaign pace became 11.8, 18.6 and 19.5 hours against a 6-hour tolerance. Not one unlucky seed — a systematic regression. The campaign runs on gear, and you can't take a third of the gear away from it. Softening the cut made it worse.
The excess was somewhere else entirely. The campaign drops 705 items in two hours — that's not a flood. The 300,000 accumulate in rifts, where quantity is multiplied by a per-tier bonus. Cutting that instead doesn't touch the campaign at all.
The result beat the original on all three measures at once: half the items, campaign pace 0.7–3.6h against the previous 0.8–4.6, and 10 of 10 builds reaching T30+ against 9.
Two things, each of which would have sunk the launch or made it worse:
/assets/… paths by default, and itch.io unpacks into /html/12345/. Players would have seen a blank page. One line fixes it (base: './'), but you can only notice by opening the built index.html and looking.
Several forks weren't technical, and the right move was to ask rather than pick:
It moves fast and it lies confidently. Both halves matter. The code arrives working and plausible; the structural mistake is invisible inside it and stays invisible until something measures it.
So the harness isn't a nice-to-have here, it's the whole safety net. Every number change gets a 120-hour simulation run compared against a stored baseline. Every class of bug that survived the build, the tests and the coverage reports got a new test written for that class — mixed-language groups, colliding entity names, Cyrillic literals in the render layer, hero color versus rarity colors.
The rule I ended up with: if a mistake can't be seen by the build, the tests or the reports, then the job isn't done until something can see it. Everything above is a receipt for that rule.
Two players left reviews on itch.io. Both were right, and neither was about balance. They were about the interface hiding half the game.
The first one ended with: "I see exactly one thing I can do. I can pick the skills for my build." He was at zone 35. He was not exaggerating — he was describing the build exactly as shipped.
The shell laid itself out with min-height: 100vh instead of a height. A CSS grid whose container height is indefinite sizes its 1fr row to content, not to the window — so the row grew to fit the sidebar, and the footer began 169 pixels below the fold. That footer holds Boost, Character, Inventory and Tree. Everything a player does with their hands, except swapping skills.
It broke on any window shorter than about 910 pixels, which includes the embed size my own publishing guide recommends. In fullscreen the page doesn't scroll, so the footer wasn't merely awkward to reach — it was unreachable.
The second review found the same class of failure one level down: the item card in the inventory was an absolutely-positioned tooltip, and the Equip button underneath was pushed clear by a hand-tuned marginTop: 220. A rare item with a comparison draws 250 pixels tall. The button was still clickable. It was just invisible, and an invisible button does not exist.
Both are the same lesson in different clothes: a layout mistake is a content mistake. Twelve uniques that change rules, a crafting economy, a passive tree — all of it was in the build the whole time, and none of it was reachable.
overflow: auto.
small · junction · small, not a single link — a direct notable-to-notable connection would cost one point, and the optimal play would instantly become "walk the rim and collect every notable". Three nodes per crossing makes the full circuit cost 36 points against a budget of 30, and it yields no keystone at all. Measured, not assumed.
Junction notables that pay off only next to both neighbours: ailment damage on attacks between Might and Flame, life leech from crits between Blood and Precision. A node whose value depends on the rest of your choices is what depth actually means. More nodes is not depth.
A fork inside every branch. After the first notable the branch splits in two and rejoins at the keystone: one side offence, the other survivability or speed. "I want that keystone" stopped meaning "I take these exact eleven nodes."
Paragon grants tree points. The tree used to close forever at level 60 — while the endgame runs for a hundred hours and paragon reaches 120. All that new depth would have been spent in the first five.pathToKeystone — used by both the balance harness and the autopsy tool — walked up connects[0] to the root. That assumed every node has exactly one parent, which was true right up until the first crossing existed. It became a breadth-first search, and the question it answers is now the interesting one: what does it actually cost to reach a keystone when you're allowed to turn.
I also deleted a test that asserted the tree has "between 60 and 90 nodes". It broke on the rework instead of guarding it — a magic number pretending to be an invariant. What replaced it measures the things that matter: the player must take under 40% of the tree, and there must be forks. Those hold no matter how the generator is rewritten next.
One decision I deliberately did not make
A full respec is free, and players say so in reviews — "good to be able to respec at anytime, experiment for free". So the new currency that removes a single node buys precision, not capability: it saves you from rebuilding fifty nodes over one mistake deep in a branch, and nothing more. Making it a real economic decision would require pricing the full respec, which would take away the thing people liked. I wrote that trade-off into the ADR rather than quietly resolving it.