You don't make a shoot-em-up hard by adding enemies. You make
it hard by knowing exactly how much fire fits on screen and how long you have to get out of it.
Difficulty is a table, not code. Ten rows and five columns: bullet speed, burst size, enemy health, wave density and the pause between waves. Tuning a stage means changing a number — and automated tests check the curve never goes backwards.
The game can be tested without opening it. The rules live in a separate Swift package that doesn't know how to draw: no SpriteKit, no screen. That's why all ten stages were balanced in a terminal, with the whole curve printed out, before touching the phone.
The expensive lesson. Starting on stage seven gave you stage one's gun: one shot every 0.17 seconds against a stage calibrated for four every 0.11. That wasn't "harder", it was seven times less firepower. Now the weapon starts at the level that stage expects.
The second expensive one. Asteroid cadence tightened according to how well you were doing — which makes sense for anything you can shoot, and none at all for anything you can't. Playing well filled your screen with indestructible plating. Obstacles now depend on the stage alone.
Haptics hang off what the game already announces. The scene already shakes the screen on every explosion, every hit and every alarm; the haptics hook in there, and the strength of the shake picks the right jolt on its own. Delete that one file tomorrow and the game is exactly what it was.
Backgrounds and music come out of a script. The ten settings are painted by a generator that lights faceted solids and closes the vertical loop seamlessly. Same for the audio: the distorted guitar is a waveshaped oscillator, EQ'd for a phone speaker — not for headphones, which was exactly what was wrong with the first mix.
Stages10, each with a boss
Lives3 · stages stay unlocked
TechnologySpriteKit + SwiftUI · 60 fps
AudioGenerated in code
Data collectedNone
PriceFree