Rogue Survivor · Build Log

The AI Cleanup

I hand-built this game engine myself: no frameworks, no AI assistance. Then I pointed AI at the code and asked one question: make it cleaner without changing how it plays. Here is everything it actually improved, shown as before/after diffs, with the playable result up top.

4
real bugs fixed
1
typo silently breaking a debug view
3
implicit globals eliminated
1
new input method: touch
0
gameplay behavior changes

Play it

The cleaned build, running live. Same game, same rules, same suffering.

Click the game once so it captures your keyboard. WASD / arrows, mouse drag, touch, or gamepad.

No formal win condition. But a happy accident: the shield orbits faster every level. Survive long enough and it becomes a blur nothing gets through. Practically invulnerable, completely unplanned. I love it.

What the AI improved

Every diff below is real, taken from the actual refactor. Nothing is staged.

1. It found a typo that silently broke a debug view

canvas.heigh is undefined, so the reticle's first line was drawn at NaN. No error, no warning. Just a quietly broken line.

Before
canvasContext.moveTo(
  canvas.width / 2 - 10,
  canvas.heigh / 2   // undefined → NaN
)
After
canvasContext.moveTo(
  canvas.width / 2 - 10,
  canvas.height / 2  // AI fix flagged in a comment
)

2. It stopped the code from leaking globals

Three assignments had no declaration at all: relativeX, relativeY, and gameOver itself. In sloppy mode they silently became globals. Plus a dozen game-state variables the original never declared.

Before
// inside gamepad movement:
relativeX = player.controller.x + player.x
relativeY = player.controller.y + player.y

// at the bottom of the file:
gameOver = () => { ... }
After
const relativeX = player.x + x
const relativeY = player.y + y

const gameOver = () => { ... }

3. It fixed the gamepad reading the wrong controller

The connect handler carefully stored event.gamepad.index. Then the poll loop ignored it and always read controller [0]. Second controller plugged in? Too bad.

Before
const currentGamepad =
  navigator.getGamepads()[0]
After
const currentGamepad =
  navigator.getGamepads()[gamepadController.index]

4. It caught a "closest enemy" check that went stale

The original cached distanceToPlayer onto each enemy object with ||, so it computed once and never again. The attract-mode AI was fleeing from ghosts.

Before
enemies.reduce((enemyA, enemyB) => {
  enemyA.distanceToPlayer =
    enemyA.distanceToPlayer || getDistance(...)
  ...
})
After
enemies.reduce((closest, enemy) =>
  getDistance(player, enemy) < getDistance(player, closest)
    ? enemy : closest)

5. It gave every magic number a name

Player HP, speeds, sizes, spawn caps, deadzones, colors: all inline literals before. Now they live in one CONFIG block, which is also where tuning the game happens.

Before
this.health = props.health || 10
this.speed = 0.005
this.color = `hsl(${12 * this.health} 100% 50%)`
// what is 12? what is 0.005? good luck.
After
this.health = props.health
  || CONFIG.player.health      // 10
this.speed = CONFIG.player.speed   // 0.005
this.color = `hsl(${CONFIG.player.huePerHealth
  * this.health} 100% 50%)`

6. It collapsed the copy-pasted menu logic

Start and Game Over menus each had the same normalized click-zone check and the same controls text. One helper each now.

Before
if (
  mouseController && mouseController.clicking
  && .4 < mouseController.x && mouseController.x < .6
  && .4 < mouseController.y && mouseController.y < .6
) newGame()
// …duplicated verbatim in EndGameMenu
After
const clickedInCenterBox = (controller) => { ... }

// StartMenu.update — and EndGameMenu.update:
if (clickedInCenterBox(mouseController)) newGame()

7. It added touch input

Phones used to rely on emulated mouse events. Now touchstart / touchmove / touchend drive the same pointer controller directly. This one is new capability, not just cleanup.

Before
// nothing — touch was emulated mouse events
// and crossed fingers
After
document.addEventListener('touchstart', (event) => {
  if (event.touches.length)
    setPointerFromTouch(event.touches[0])
}, { passive: true })
// + touchmove, touchend

8. It documented the weirdest design decision instead of "fixing" it

The keyboard handler looks inverted: pressing right moves enemies left. A naive refactor would "correct" this into a bug. The AI recognized the actual design: the player stays centered and input shifts the world around them.

Before
entity.x += (keysController.right
  ? -player.speed : 0)  // inverted?!
  + (keysController.left ? player.speed : 0)
After
// The player stays near screen center; input
// shifts the world around them. Pressing right
// moves every enemy left — reading as the
// player moving right.
if (keysController.right) enemy.x -= player.speed
What the AI didn't touch: the architecture. Entity → Ball → PlayerBall / EnemyBall, the three input controllers, the menu class hierarchy, the wave spawner: all mine, all intact. The refactor renamed, extracted, and documented. It never redesigned. That was the deal, and honestly, it is the correct use of the tool: I made the decisions, it did the tidying.