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.
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.
Every diff below is real, taken from the actual refactor. Nothing is staged.
canvas.heigh is undefined, so the reticle's first line was drawn at NaN. No error, no warning. Just a quietly broken line.
canvasContext.moveTo(
canvas.width / 2 - 10,
canvas.heigh / 2 // undefined → NaN
)canvasContext.moveTo(
canvas.width / 2 - 10,
canvas.height / 2 // AI fix flagged in a comment
)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.
// inside gamepad movement:
relativeX = player.controller.x + player.x
relativeY = player.controller.y + player.y
// at the bottom of the file:
gameOver = () => { ... }const relativeX = player.x + x
const relativeY = player.y + y
const gameOver = () => { ... }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.
const currentGamepad =
navigator.getGamepads()[0]const currentGamepad =
navigator.getGamepads()[gamepadController.index]The original cached distanceToPlayer onto each enemy object with ||, so it computed once and never again. The attract-mode AI was fleeing from ghosts.
enemies.reduce((enemyA, enemyB) => {
enemyA.distanceToPlayer =
enemyA.distanceToPlayer || getDistance(...)
...
})enemies.reduce((closest, enemy) =>
getDistance(player, enemy) < getDistance(player, closest)
? enemy : closest)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.
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.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%)`Start and Game Over menus each had the same normalized click-zone check and the same controls text. One helper each now.
if (
mouseController && mouseController.clicking
&& .4 < mouseController.x && mouseController.x < .6
&& .4 < mouseController.y && mouseController.y < .6
) newGame()
// …duplicated verbatim in EndGameMenuconst clickedInCenterBox = (controller) => { ... }
// StartMenu.update — and EndGameMenu.update:
if (clickedInCenterBox(mouseController)) newGame()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.
// nothing — touch was emulated mouse events
// and crossed fingersdocument.addEventListener('touchstart', (event) => {
if (event.touches.length)
setPointerFromTouch(event.touches[0])
}, { passive: true })
// + touchmove, touchendThe 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.
entity.x += (keysController.right
? -player.speed : 0) // inverted?!
+ (keysController.left ? player.speed : 0)// 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