← All dev notes

Small bugs that quietly changed how my game played

12 October 2026

None of these bugs crashed anything. The game ran fine with every one of them. What they did was quietly change how it played, in ways I'd never have picked up from testing because I knew what the game was supposed to do and my eyes filled in the rest. Each fix was a line or two. Finding them was the hard part.

Swinging at nothing filled the Ki bar

The Ki bar powers the special attack, and it's meant to fill as you land hits. The How to Play page says exactly that. A player told me it filled just as fast if you stood in an empty corner swinging at the air.

They were right. The attack code looked roughly like this:

let hit = false;
enemies.forEach(e => {
  if (!inRange(e)) return;
  e.hp -= damage;
  hit = true;
});

combo++;
ki += 3;

There was already a hit flag that recorded whether the swing connected. The two lines that rewarded you sat right under it and never checked it. So the combo counter and the Ki bar went up on every swing, hit or miss.

The fix was wrapping those two lines in if (hit). What I find interesting is the effect it had. The combo multiplier adds damage, and the special does a big fixed hit to everything on screen. With the bug in, the best strategy was to mash attack all the time, because missing cost you nothing. With it fixed, aiming matters. It's a different game, and the one I thought I'd made.

91 boss fights against the same boss

There are ten named bosses and a boss every tenth stage across 1000 stages. The code picking which boss to use was:

const i = Math.min(Math.floor(stage / 10) - 1, 9);

Stage 10 gives 0, the first boss. Stage 100 gives 9, the tenth. Stage 110 would give 10, but the Math.min caps it at 9. So would stage 120, and stage 500, and stage 1000.

Every boss from stage 100 to stage 1000 was the last boss, Death Itself. That's 91 fights in a row against the same opponent. I'd written the clamp to stop the index running off the end of the array, which it did, and I never thought about what happens once you get there.

The fix is to wrap around instead of clamping:

const i = (Math.floor(stage / 10) - 1) % 10;

Now stage 110 is the first boss again, with more health. The roster cycles ten times over a full run. Most players haven't reached stage 100 yet, so not many people ever saw the old version, but anyone who did would have reasonably assumed the game ran out of content.

Swings that looked like they hit and didn't

This is the one that took longest to find, because the complaint was so vague. The report was that some swings visibly went through an enemy and did nothing, and that it was worst in mid-air.

Each of the three combo attacks was defined with a few numbers for the hit area:

{ name: 'Finisher', hbX: 70, hbY: -75, hbW: 120, hbH: 130, ... }

hbW was supposed to be the width. It was declared on all three moves and never read anywhere. The function building the hit box took its width from hbX, which is how far forward the box starts. So every attack had a hit area of the wrong size, and nothing complained, because a wrong number is still a number.

Underneath that was a bigger problem. The slash you see on screen is drawn from a separate set of numbers describing the arc. The hit box and the drawn arc had been tuned separately and had drifted apart. The finisher's visible arc reached 32 pixels further forward and 50 higher than anything it could actually hit. On the ground you usually got a second swing in. In the air, you'd fall past the enemy before you did.

The fix was to stop having two sources of truth. The hit box is now worked out from the same arc numbers that draw the slash, by sampling points around the sweep and taking the box that contains them. If the picture changes, the hit area changes with it. They can't disagree any more.

What these have in common

Every one of these was two pieces of code that were supposed to agree, with nothing checking that they did. The hit box is the clearest example, but the Ki bug is the same thing in a smaller form: the code knew whether you'd hit, and the reward just didn't ask. These days when I add something that has to match something else, I try to make one of them come from the other, so there's only one number to get wrong.

Two of the three were also found by players, not by me. If someone tells you your game feels off in a way you can't reproduce, believe them first and go looking second.

Play Samurai Vengeance