Claude Code · VS Code · Agent Teams · Project Three

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.

Written for Tommy · FrameLogic Studio · August 2026 · Assumes the Brick Break tutorial (Stages 0–4); pairs with Maze Muncher as a second-project alternative. Field fixes from the first live run are already applied. Verified against Claude Code v2.1.240.

The formation fills as you progress · magenta row is the boss rank · Stage 4's team builds the dives

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

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)

RankRoleBehavior
DronefodderDives 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.
Lancermid rankSteeper dives, aimed shots, occasionally kamikaze-continues instead of returning.
Bosstop rank, 2 HPChanges 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

MilestoneScopeBuilt by
M1 FormationShip moves and fires; enemies pre-placed in a breathing formation; collisions; score; lives; scrolling starfield.Solo (Stage 1)
M2 EntrancesSpline paths; the wave file format; stage 1's entrance choreography; formation assembles slot by slot; stage-clear → next stage.Solo + subagents (Stage 2)
M3 TuningSpeeds, fire rates, breathing amplitude, path shapes — reviewed; the dive/capture spec written.Review team (Stage 3)
M4 The attackDive scheduling, aimed fire, boss escorts, tractor beam, capture, rescue, dual fighter.Build team (Stage 4)
M5 Challenge packBonus 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:

Prompt — scaffold, plan mode first
Scaffold a browser fixed-shooter called star-swarm with this exact directory layout: [paste the tree]. Constraints: plain HTML/CSS/JS ES modules, no framework, no build step; canvas 2D; server.js = minimal Express static server on port 3000; npm test runs node --test over tests/. Everything in src/logic and src/levels is pure — no DOM, window, canvas, Date.now, or Math.random; callers inject clock time and rng. Build milestone 1 only: player ship moves left/right (arrows) clamped to screen, fires (space, max 2 player bullets alive), a formation of 40 enemies pre-placed in 5 ranks (4 bosses with 2 HP that change tint at 1 HP, 8+8 lancers, 10+10 drones) whose slot positions come from a pure formation.js with breathing (slow scale oscillation) and side-to-side drift, both functions of an injected clock. Bullet-enemy and (stub) enemy-player collision in a pure collide.js. Score per rank, 3 lives, stage-clear screen when the formation is empty, scrolling starfield background. Original enemy designs drawn with canvas shapes — geometric, readable at 24px, no sprites copied from anything. Unit tests: formation slot math (breathing amplitude, drift bounds, slotPos determinism for a fixed clock), collide.js hit/miss cases, boss 2-HP behavior. Integration: a pure sim that fires until the formation is empty and asserts stage-clear. CLAUDE.md: the pure boundary, directory ownership, one branch per feature, conventional commits, npm test before completing any task. docs/ROADMAP.md gets the five milestones I'll paste next. Make npm test pass. Show me the plan before writing anything.

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.

Prompt — M2 via subagents
Work on branch feat/m2-entrances. Use the explorer subagent to report how formation, entities, and main.js connect and the cleanest seam for "an enemy is either on a path or in its slot." Then use the implementer subagent for src/logic only: paths.js — Catmull-Rom spline through named control-point lists, arc-length reparameterized so pathPoint(path, t) moves at constant speed, plus mirrorPath(path) reflecting across the vertical centerline; waves.js — parse and validate the wave format we'll write in docs/specs/wave-format.md (groups, type, named path, launch interval, target slots; reject unknown paths/slots/overfilled slots); the arrival blend that retargets the last 15% of a path to the slot's position at projected arrival time. Then the implementer subagent again for wiring: enemy state machine entering→settling→inFormation, staged launches per the wave file, stage 1 defined in src/levels/stages.js using 4 waves and at least 3 named paths (one mirrored pair), stage-clear advances to stage 2 (same waves, +10% speeds for now). Unit tests: constant-speed property (equal-t samples equal distances within tolerance), mirror symmetry, wave validation rejections, arrival lands within 2px of the moving slot in a fixed-clock sim. Then write docs/specs/wave-format.md documenting the format for future projects. Finally the reviewer subagent on the full diff; show me all reports.

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.

Prompt — review team
Create an agent team to review this repo. Spawn three teammates named feel, bugs, and threat. Use Sonnet for each. - feel: ship speed vs formation width, the 2-bullet limit's rhythm, entrance path shapes and speeds, breathing amplitude, starfield speed — concrete numbers with reasoning. - bugs: path edge cases (t past the end, degenerate control points), arrival-blend failures at extreme drift, slot double-assignment, collision during entrance, score/lives correctness. - threat: right now nothing attacks — the game is target practice. Specify what dives, aimed fire, boss escorts, and the tractor-beam capture/rescue must each add, with the full capture state machine (including the captive-destroyed-if-boss-killed-in-formation rule and the dual fighter), as input to docs/specs/attack.md. Message each other where findings overlap; report with severity and file:line; wait for all three; then give me a tuning changelist to approve and a draft attack.md. Do not change any files.

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:

Prompt — build team: the attack
Create an agent team to build docs/specs/attack.md. Spawn four teammates with these agent types and names; give each ONLY the files listed; put the ownership map in the shared task list. - brains (implementer, require plan approval): owns src/logic/dives.js and src/logic/capture.js — dive scheduling (cadence and group-size rules from the wave file, rng-injected; picks divers by rank rules; generates dive paths from templates parameterized by the player's position at launch; return paths that end at slotPos at projected arrival), aimed-fire solutions, escort slot selection, and the complete capture state machine from the spec including the captive-destroyed rule and dual-fighter merge/split. Everything pure, clock+rng injected. Only approve a plan with exact signatures and a test per state transition. - swarm (implementer): owns src/entities/** and src/main.js — enemy dive/return states driven by brains' outputs, enemy bullets, the boss beaming stop, the player captured/respawn flow, dual-fighter entity (two hitboxes, two shot origins, drop to single on hit). Message brains to agree on signatures before coding. - face (implementer): owns src/ui/**, src/engine/draw.js, src/engine/starfield.js, and public/** — the beam cone (animated, original design), captured ship rendered above the boss, dual-fighter visuals, "FIGHTER CAPTURED" and rescue moments on screens.js, boss-damage tint. - qa (reviewer then implementer): owns tests/** — table-driven tests over every capture state transition including both boss-death branches and player-death-during-rescue; seeded-rng dive cadence within bounds; dive paths launch and return within tolerance in fixed-clock sims; aimed shots intersect the player's launch-time position; dual fighter drops to single correctly. Reviews each diff before tasks complete. Rules: branches feat/attack-<name>; no edits outside owned files — message the owner; 5–6 tasks each with dependencies (swarm and face depend on brains' plan approval); wait for all before summarizing; implement nothing yourself.

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.

Prompt — to pm-stages
You are the project lead for the challenge pack in this worktree. Run an agent team that delivers: three challenge stages in the documented wave format (docs/specs/wave-format.md) — every enemy enters, flies a looping showpiece path, and exits; no dives, no firing; 40 enemies each; a perfect-clear bonus of 10000 —, at least four new named paths including one full-screen figure, and a standalone wave-file validator (npm run validate-waves) that checks path references, slot assignment, group timing overlaps, and challenge-stage rules. Require plan approval for any change to the wave format itself; format changes must be backward compatible and routed through the leads to pm-game. A stage ships only when qa proves it validates and a fixed-clock sim completes it with zero collisions among enemies. Report to lead-a per protocol. Start now.

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

SymptomLikely causeFix
Enemies speed up on curvesSpline evaluated by raw t, not arc lengthThe 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 slotPath endpoint aimed at the slot's position at launch, not arrivalThe arrival blend must target slotPos at projected arrival time; re-check the last-15% retarget
Two enemies claim one slotSlot assignment done at arrival instead of launchAssign slots when the wave file is parsed; the validator should reject overfilled slots
Dives feel scripted/identicalrng not injected, or cadence bounds collapsedGrep src/logic for Math.random; check the seeded-cadence test asserts on a sequence
Rescue never triggersBoss-killed-while-diving check runs after the state already flipped to ascending/inFormationThe 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 originMerged entity kept a single shot originTwo origins, two hitboxes, independent hits; qa's drop-to-single test covers it
Everything from the earlier guidesBrick 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

Credits

Written by Tommy Waddell (FrameLogic Studio) with Claude (Anthropic). Multi-agent architecture based on the daily-driver workflow described by Daisy, an engineer on the Claude Code team, as quoted in an Anthropic email newsletter (2026). Game mechanics are drawn from the publicly documented design of the Galaga family of fixed shooters; Star Swarm uses original names, art, and stage choreography and is not affiliated with or endorsed by Bandai Namco. Verified against Claude Code v2.1.240, August 2026. Agent teams are experimental — trust your screen over this document where they differ.