Dev Log: 2026-10-08¶
Build 0.1.9.111. Two days ago I wrote that one report, a dead unit's header snapping back to its old HP, was hardened but not solved. It is solved now, and the cause was not where I had been looking. The rest of this post is the second round with the AI playtester: chapters 2 and 3, a soft-lock, Alpha Katyusha's Iron Curtain, and a score ceiling that said no to a perfectly honest clear.
The header that kept reverting¶
The tester came back on build 0.1.9.110 with a clean reproduction. Nadeshiko died on a route. The header showed 0 HP. Then it pressed K, and her header went back to 215/344 while she stayed dead and greyed out on the map.
That detail, "pressed K", was the clue I lacked last time. Selecting a unit refreshes the damage previews, and clearing a preview put the unit's HP text back like this:
if GameManager.units.has(unit_id):
var state: Dictionary = GameManager.units[unit_id]
p.hp_value.text = tr("HUD_VALUE_FRACTION").format({"cur": state.current_hp, "max": state.max_hp})
That reads the saved HP, and autosave deliberately skips dead units, so the save still held the pre-death number. Every preview refresh quietly repainted the panel from stale data. It also hit living units: between autosaves their saved HP lags the live one, so a header could show an older value right after you clicked around.
The fix is smaller than the bug. A preview now remembers the text it overwrote and puts exactly that back, and every method that paints the HP for real throws the remembered text away.
if p.hp_text_stash == null:
p.hp_text_stash = p.hp_value.text
p.hp_value.text = tr("HUD_VALUE_FRACTION").format({"cur": after_hp, "max": unit.max_hp})
The regression test fails on the old code with the tester's exact number, got 215 / 344. My earlier hardening was not wasted, since it fixed two real paths, but it addressed the wrong one for this symptom.
A soft-lock with a boring cause¶
A different report was alarming. In chapter 3 stage 10 the tester attacked Alpha Katyusha, whose Iron Curtain was up, and the game froze on "re-targeting". It clicked UNDO and CANCEL to get out. The next EXECUTE hid its buttons and then did nothing at all: no move, no shot, no enemy phase, and every click on the board ignored while the pause menu still worked. A reload recovered it.
Cancelling a re-targeting attack raised a flag, _queue_execute_cancelled, meant for a running queue batch. A batch resets it when it ends. Outside a batch, nothing did. So the flag stayed up, and the next plain route checked it on its very first step, bailed out, and left the game in its animating phase.
# Only a running queue batch owns _queue_execute_cancelled (it resets it
# when the batch ends). Raised outside a batch it stuck forever...
if _queue_executing:
_request_queue_execute_cancel()
Now only a real batch raises it, and a plain route clears any stale one when it starts. The tests reproduce the old failure, a route that is silently not walked.
I could not reproduce the first half, the "re-targeting" message hanging with pieces flickering. My guess is a pursuing boss making the route and the re-target code call each other over and over, so there is now a limit of five re-targets before control goes back to you with "Can't reach target, re-plan". That one is a guard, not a proven fix, and the issue says so.
Iron Curtain you can read¶
Alpha Katyusha's Iron Curtain blocks every hit and reflects it back at the attacker. The tester found it confusing for three separate reasons, and all three were fair.
- The preview hid the cost. Attacking a curtained boss previewed no change for either side, then the attacker lost hundreds of HP to the reflect. Attack previews now include it: the boss's ATK minus the attacker's defense, which is exactly what the counter deals. The shot itself is still blocked, so the boss's own HP stays put.
- Limit breaks had the same gap. Storm Run and Broadside previews now show the reflect on the caster's header too.
- Nothing said whether it was up. The boss had a pulsing outline, but the hover tooltip and the long-press panel did not. Both now read
IRON CURTAIN 1/2while it is up (blocks used out of the cap) and the charge while it fills.
Two other reports were not bugs. The Nexus counter-pulses everything within range 5 whenever it is damaged, and that pulse ignores Iron Curtain by design, so "damage got through" next to it is correct. And a curtain that shows 5/6 at the start of the next sector is not stale. The code never resets it between sectors, so it really is still up with one block left. The tester agreed after checking.
A clear that was rejected¶
On chapter 2 stage 10 the tester finished with 92,349 points and the leaderboard answered "flagged as implausible." The score was real. It had killed all 46 drones.
The server rejects scores above a per-stage ceiling, and that ceiling is built from the solver's reference clear: its kills, then headroom on damage and actions. On a boss stage the solver kills only what it needs, here 14 of 46, so the ceiling was 30,462 and any player who cleared everything was over it.
The ceiling is now raised to what the same clear would score if the player killed every drone, plus the escorts a summoning boss keeps spawning, with the same damage and action headroom as before. It only ever goes up, and hand-tuned stages are left alone. I did not want to rerun the solver just to widen a bound, and the stored kill count turns out to be recoverable from two numbers already in the targets file, so the new module inverts them and checks that the result is a whole number of kills no larger than the drones in the stage. Seven stages widened. Chapter 2 stage 10 is now 140,466.
Smaller things¶
- Clicking another unit during a route preview used to do nothing, so the old unit looked stuck as selected. It now drops the plan and switches, like its unit key already did.
- One set of chapter titles. The world map, the credits, the chapter files and the design docs had drifted: chapter 2 said "What Did We Build?" on the map and "What Are We?" in the credits, chapter 3 had three different titles, chapter 5 had two. They now agree in all three languages: Where Are We?, What Are We?, What Did We Know?, Who Did We Hurt?, What Did We Hide?, What Do We Do Now?
- The chapter complete card is translated. Its text was hardcoded English, and the demo line still said "Chapters 2 through 5" when there are six.
- The Stage 10 tile shows its stars. It was drawn by its own function that never passed the star count along.
Bug fixes¶
- A fallen unit's header reverted to its pre-death HP, and living units could show an older HP after a preview refresh.
- Soft-lock after cancelling a re-targeting attack: EXECUTE did nothing and the board stopped responding.
- Attack and limit break previews missed the Iron Curtain reflect from Alpha Katyusha.
- Alpha Katyusha's curtain was invisible in the hover tooltip and the inspect panel.
- Clicking another unit during a route preview did nothing.
- A legitimate boss-stage clear was rejected as implausible. Needs the leaderboard server to be updated, see below.
- The Stage 10 world map tile showed no stars.
- Chapter titles disagreed between the map, the credits and the docs, and the chapter complete card was English only.
Miscellaneous¶
- New tests for the preview stash, the cancel flag and the re-target limit, clicking between units mid-route, the reflect in attack and limit break previews, the curtain readouts, the star label, the chapter complete strings, and the score ceilings.
- GDScript test suite: 1,591 passing (up from 1,562).
- Python test suite: 689 tests, all passing (18 skipped: opt-in slow suites).
- The ceiling change is regenerated in the repo, but the self-hosted leaderboard server is refusing SSH commands at the moment, so the new table is not deployed yet. I have asked the host's admin to look at it.
- Build 0.1.9.111 goes out to Steam, Google Play, and itch.io.
I keep relearning that the quick explanation is often the wrong layer. A header that reverts looks like a drawing bug, so I hardened the drawing. A frozen board looks like an animation hanging, but it was a cancel flag that nobody ever put down. A rejected score looks like a cheat check doing its job, but it was a ceiling built from one clear and applied to every other way of playing. In each case the useful move was to take the tester's exact numbers and the key it pressed, and ask what the game would have to be holding for those to be true.