Star Swarm
Build a Galaga-style fixed shooter with a team of AI agents
The third practice project in the series, and the most choreographed: enemies that swoop in along curved flight paths, assemble into a breathing formation, peel off in coordinated dive attacks — and a boss whose tractor beam can steal your ship, setting up the series' best moment: the rescue that gives you a dual fighter. All of it built with the same agent workflow, and all of that choreography expressed as pure, testable math.
Same IP rule as Maze Muncher. Galaga is Bandai Namco's property: the name, the insect sprites, the sounds, and the exact stage tables are protected. The mechanics — entrance flights, a formation that breathes, dive attacks, a capture beam and a rescued dual fighter — are ideas, free to learn from and reimplement. This tutorial builds Star Swarm with original names, original enemy designs, and your own wave choreography. Don't ship it as "Galaga," don't trace the sprites, and don't copy the original wave tables verbatim.
- Why this game teaches something new
- Prerequisites
- The design: paths, formation, dives, and the beam
- Stage 1 — Scaffold: ship, shots, formation, collisions
- Stage 2 — Subagents: entrance flights and the wave format
- Stage 3 — Review team: difficulty and the dive spec
- Stage 4 — Agent team builds dives, the beam, and the rescue
- Stage 5 — Challenge-stage pack in a worktree
- Troubleshooting specific to this project
- Credits
Why this game teaches something new
Each project in this series adds one hard thing. Brick Break added the workflow itself. Maze Muncher added interacting AI personalities on a grid. Star Swarm adds choreography over time: nothing in this game reacts to you the way a wisp does — instead, dozens of enemies execute timed, curved, coordinated movements defined as data. The engineering lesson is that flight paths, wave scripts, and dive schedules are all pure functions of time: positionAt(path, t). Once movement is a function of time, everything becomes unit-testable — "at t=1.2s this enemy is within 2px of (140, 88)" — and your QA teammate can verify an entire attack wave without a browser.
It also adds the best state-machine problem in the series: the capture sequence. Boss fires beam → your ship is dragged up → you lose a life but the boss now flies with your captured ship → shoot that boss while it's diving and the ship descends and docks alongside you → dual fighter, double shots, twice the hitbox. That's five states, three entities, and a dozen edge cases (what if the boss dies in formation? what if you die during the rescue?) — exactly the kind of feature that goes wrong without a spec and a QA gate, which is why it's the Stage 4 team build.
Prerequisites
Identical to Maze Muncher's list: CLI v2.1.240+ signed in, agent-teams flag on, your three roles (with SendMessage) in ~/.claude/agents/, your settings template with :* permission syntax and the two hooks, GitHub + gh.
mkdir -p ~/code/star-swarm && cd ~/code/star-swarm git init -b main gh repo create star-swarm --private --source=. --remote=origin mkdir -p .claude docs/specs cp ~/claude-templates/settings.json .claude/settings.json code .
Standing reminders: role files load at session start; don't leave settings.json open unsaved while Claude writes it; plan mode for anything that defines a shape others build on.
The design: paths, formation, dives, and the beam
Everything moves on splines
A flight path is a list of control points; a pure module evaluates it: pathPoint(path, t) returns position and heading at time t (Catmull-Rom through the points, constant-speed reparameterized so enemies don't speed up on curves). Entrance flights, dive attacks, the beam descent, and the rescued ship's docking approach are all just paths. Paths are data — defined in the wave files, mirrored automatically for symmetric entrances — so designing an attack never touches code.
The formation breathes and is the source of truth
The formation is a grid of slots (5 rows: one boss rank of 4, two ranks of 8, two of 10). It slowly expands and contracts ("breathing") and slides side to side; a slot's world position is slotPos(slot, t) — again pure, again a function of time. Every enemy is always either on a path (entering, diving, returning) or in its slot. Returning enemies fly a path whose endpoint is the slot's future position, which is the one genuinely tricky interpolation in the game and the reason slotPos takes t.
Waves are files
# docs/specs/wave-format.md (sketch — Stage 2 finalizes it) stage: waves: # entrance choreography, in order - group: 8 # enemies enter in pairs along a path type: drone path: swoopLeft # named path from paths.js, auto-mirrorable interval: 0.25 # seconds between launches slots: [r3s0..r3s7] dives: # after assembly: scheduling rules, not scripts cadence: [4, 8] # seconds between dive launches, min..max (rng) groupSize: [1, 3] escorts: true # bosses may bring 1–2 drones from adjacent slots
Enemy ranks (original designs — invent the look, keep the roles)
| Rank | Role | Behavior |
|---|---|---|
| Drone | fodder | Dives solo or in pairs on a curved strafe, fires 0–2 shots at your position-at-launch, loops off-screen bottom and re-enters to its slot. |
| Lancer | mid rank | Steeper dives, aimed shots, occasionally kamikaze-continues instead of returning. |
| Boss | top rank, 2 HP | Changes color at 1 HP. Dives with up to two drone escorts in delta formation. May stop mid-dive to fire the tractor beam. |
The capture state machine (Stage 4's crown jewel)
BOSS: inFormation → diving → beaming (stops, cone sweeps) → ascending (with captured ship) → inFormation*
PLAYER: flying → captured (dragged up; costs a life; next ship spawns) → —
CAPTIVE SHIP: heldByBoss → descending (boss killed while diving) → docking → merged (dual fighter)
→ destroyed (boss killed in formation — the cruel path; keep it, it's the real rule)
Dual fighter: two ships side by side, both fire, both can be hit; losing one drops you back to single. Rescue only works if you kill the boss while it's diving; killing it in formation destroys the captive. Original-faithful, and it makes the beam a genuine risk/reward decision.
Layout
star-swarm/ ├── CLAUDE.md · .claude/ · docs/ (ROADMAP · STATUS · specs/) ├── public/ index.html · style.css ├── src/ │ ├── engine/ loop.js · input.js · draw.js · starfield.js │ ├── logic/ paths.js · formation.js · waves.js · dives.js · capture.js · collide.js ← pure, rng+clock injected │ ├── entities/ player.js · enemy.js · bullet.js · beam.js │ ├── levels/ stages.js (wave data) · parse.js ← pure │ ├── ui/ hud.js · screens.js │ └── main.js ├── tests/ unit/ · integration/ ├── server.js · package.json
Roadmap
| Milestone | Scope | Built by |
|---|---|---|
| M1 Formation | Ship moves and fires; enemies pre-placed in a breathing formation; collisions; score; lives; scrolling starfield. | Solo (Stage 1) |
| M2 Entrances | Spline paths; the wave file format; stage 1's entrance choreography; formation assembles slot by slot; stage-clear → next stage. | Solo + subagents (Stage 2) |
| M3 Tuning | Speeds, fire rates, breathing amplitude, path shapes — reviewed; the dive/capture spec written. | Review team (Stage 3) |
| M4 The attack | Dive scheduling, aimed fire, boss escorts, tractor beam, capture, rescue, dual fighter. | Build team (Stage 4) |
| M5 Challenge pack | Bonus stages (no-fire choreography waves, perfect-clear bonus) as a wave pack in its own worktree, plus a wave-file validator. | Two PMs + leads (Stage 5) |
1Scaffold: ship, shots, formation, collisions
Goal: M1 playable — you strafe and shoot a breathing formation out of the sky over a scrolling starfield.
claude --name solo, plan mode, paste:
Approve, build, npm install && npm start, play. The feel checks: your two-bullet limit forces rhythm (that's the classic constraint — keep it), and the formation's breathing is visible but subtle. Paste the roadmap, commit, merge, push.
Check yourself: formation breathes; bosses take two hits and visibly change; stage-clear fires; tests green; pushed.
2Subagents: entrance flights and the wave format
Goal: M2 — the formation now assembles: enemies swoop in along curved paths in choreographed groups, defined entirely by wave files.
What you'll learn: speccing a data format precisely (it becomes Stage 5's cross-project contract), and testing motion as math.
Watch a full assembly before touching anything else — the moment groups peel in from both sides and click into a breathing grid is the game arriving. Commit, merge, push.
Check yourself: enemies enter in visibly choreographed groups; late arrivals land in moving slots without popping; wave-format.md exists and matches the validator; tests green.
3Review team: difficulty and the dive spec
Goal: tuned M3 plus a written docs/specs/attack.md — the spec Stage 4's team builds from.
Approve the tuning, then: "Apply the changelist, write the final docs/specs/attack.md, commit both, push, shut the team down." Read attack.md yourself — the capture machine's edge cases are the spec's whole job.
4The agent team builds dives, the beam, and the rescue
Goal: M4 shipped by a four-teammate team: the swarm fights back, the beam steals your ship, and the rescue pays it off with a dual fighter.
Hooks confirmed in settings, fresh session, paste:
Same operating beats as always: Ctrl+T, the plan-approval exchange, hook rejections doing their job. Merge in order brains → swarm → face → qa with tests between; then play for the real acceptance test: let the beam take your ship on purpose, kill that boss mid-dive, and fly the dual fighter. If the rescue ever feels ambiguous — you can't tell whether you're about to save or destroy your ship — that's a face problem (readability), not a brains problem; route feedback to the right owner.
Check yourself: dives launch on the file's cadence; bosses bring escorts; the full capture → rescue → dual fighter loop works; killing a beaming boss's formation-mate does not free the ship (the cruel rule holds); qa's table tests cover every transition; merged, green, pushed.
5Challenge-stage pack in a worktree
Goal: the two-project stack: pm-game polishes (sounds, screens, high scores), while pm-stages builds a challenge pack — bonus stages where enemies fly pure choreography, never attack, and a perfect clear pays a bonus — in its own worktree. The wave format from Stage 2 is the contract between them.
Two-person mode: one of you takes each PM, and the wave format becomes a genuine negotiation — pm-stages will want format extensions (loop counts, exit paths) that pm-game has to consume. Let the leads carry it.
Troubleshooting specific to this project
| Symptom | Likely cause | Fix |
|---|---|---|
| Enemies speed up on curves | Spline evaluated by raw t, not arc length | The constant-speed reparameterization is the fix; its unit test (equal-t steps = equal distances) should be failing — check it isn't tolerance-loosened |
| Enemies "pop" when reaching their slot | Path endpoint aimed at the slot's position at launch, not arrival | The arrival blend must target slotPos at projected arrival time; re-check the last-15% retarget |
| Two enemies claim one slot | Slot assignment done at arrival instead of launch | Assign slots when the wave file is parsed; the validator should reject overfilled slots |
| Dives feel scripted/identical | rng not injected, or cadence bounds collapsed | Grep src/logic for Math.random; check the seeded-cadence test asserts on a sequence |
| Rescue never triggers | Boss-killed-while-diving check runs after the state already flipped to ascending/inFormation | The capture table test for that transition is the pin; if it passes but play fails, the wiring in swarm's main.js isn't consulting capture.js — message that owner |
| Dual fighter fires from one origin | Merged entity kept a single shot origin | Two origins, two hitboxes, independent hits; qa's drop-to-single test covers it |
| Everything from the earlier guides | — | Brick Break's Troubleshooting and Field Notes still apply: roles load at session start, stale-tab problem, hook syntax, ownership drift caught via git diff --stat |