The pack
flightFormation
function flightFormation(): Pack<{ flightFormation: FlightFormationApi }>
Install the renderer-independent flight-formation rules (Galaga).
Helpers
createGalaga
function createGalaga(config: GalagaConfig)
Create an independent flight-formation game. No renderer or kernel clock is required.
galagaEntry
function galagaEntry(slots: GalagaSlot[], size = 8, waveGap = 3): EntryWave[]
Entry groups for any slot list: groups of size in slot order, cycling the five arcade approaches.
galagaPreset
function galagaPreset(o: GalagaPresetOptions = {}): GalagaConfig
Galaga (Namco, 1981) as data. Classic values: 224x288 screen; 4 Boss Galaga, 16 butterflies (Goei) and 20 bees (Zako) entering in five groups of eight; sway while arriving, then breathing; bees 50/100, butterflies 80/160, bosses 150 in formation and 400/800/1600 diving with 0/1/2 escorts already shot, a captured fighter 500/1000; bosses take two hits; tractor-beam capture and rescue into a dual fighter; challenging stages 3, 7, 11... with 40 one-hit flyers, 100 per hit, 10,000 for all 40 and group bonuses of 1,000-3,000; two missiles in flight; extra fighters at 20,000, 70,000 and every 70,000. Flight programs are reference-style approximations, not ROM tables.
galagaPrograms
function galagaPrograms(): Record<string, FlightProgram>
Flight programs of the classic preset. Dives are authored for the RIGHT half; the left half mirrors them.
galagaSlots
function galagaSlots(rows: GalagaRow[])
Slots for centred rows on a grid as wide as the widest row.
ramp
function ramp(r: Ramp, stage: number)
Evaluate a ramp at a 1-based stage.
Classes
Galaga
class Galaga
Owns a flight-formation game: entries, formation sway/breathing, dives, escorts, capture, rescue, challenging stages.
readonly config: GalagaConfig— The validated definition (a private copy).constructor(config: GalagaConfig)isChallenging(stage: number)— Is this 1-based stage a challenging stage?get state(): GalagaState— A copy of the whole state.slotPosition(slotId: string, offsetY = 0)— Where a slot sits right now, including sway and breathing.get beams()— Active beams for drawing.tick(seconds: number, input: GalagaInput = {})— Advance by real seconds in fixed steps.step(input: GalagaInput = {})— Advance exactly one fixed step.launchDive(id?: string): string | null— Launch a dive now by the configured selection rules; returns the leader id or null.drain()— Take the events since the last drain.snapshot()— Serializable snapshot of the whole game.restore(snap: ReturnType<Galaga["snapshot"]>)— Restore a snapshot taken from the same configuration.reset(stage = 1)— New game fromstage(default 1).
Types
EntryWave
interface EntryWave
One entry group: members leave from start along program, gap seconds apart (or in pairs).
members: string[]— Slot ids (normal stages) or kind ids (challenging stages).program: string— Flight program the members fly.start: { x: number; y: number }— Where the first member appears.heading: number— Initial heading in degrees (0 right, 90 down).mirror?: boolean— Mirror the program (negated turns, heading reflected).pairs?: "mirror" | "abreast"— Members fly two abreast, the second mirrored about the screen centre (top entries) or offset.gap: number— Seconds between members (or pairs).at: number— Seconds after the stage's entry starts.
FlightEnd
type FlightEnd = "formation" | "wrap" | "exit" | "beam"
How a program ends.
FlightFormationApi
type FlightFormationApi = typeof galaga & typeof presets
game.flightFormation: the flight-formation rules owner, its preset and its builders.
FlightProgram
interface FlightProgram
A named program and its ending.
steps: FlightStep[]— Steps flown in order.then: FlightEnd— What the flyer does when the steps run out.
FlightStep
interface FlightStep
One step of a flight program.
seconds: number— Duration of the step.turn?: number— Degrees per second, positive = clockwise on screen. Negated when the program is mirrored.speed?: number— Multiplies the flight's base speed. Default 1.aim?: number— Steer toward the player at up to this many degrees per second.untilY?: number— End the step early once the flyer is at or below this y.fire?: boolean— Shots may be fired during this step (up to the dive'sperDivebudget,gapapart, within the fire band).
GalagaConfig
interface GalagaConfig
Complete flight-formation definition; galagaPreset builds the arcade one.
width: number— Playfield width in model pixels.height: number— Playfield height in model pixels.hz: number— Fixed simulation steps per second.seed?: number— Seed for selection, capture and entry-fire chances.kinds: GalagaKind[]— Enemy kinds.formation: { slots: GalagaSlot[]; /** Grid width in columns (slot cols index into it). */ columns: number; centerX: number; /** Centre y of row 0. */ top: number; spacingX: number; spacingY: number; /** Side-to-side drift while the fleet is still arriving. */ sway: { amplitude: number; period: number }; /** Expansion and contraction once every enemy has arrived (fraction of spacing). */ breathe: { amount: number; period: number }; }— Formation grid, slots and motion.programs: Record<string, FlightProgram>— Named flight programs for entries, dives, captures and challenging stages.stages: { /** First challenging stage (null = none) and the interval after it. Original: 3 and 4. */ challengingFirst: number | null; challengingEvery: number; /** Entry waves for normal stages. */ entry: EntryWave[]; /** Challenging stage wave sets, cycled by challenging-stage index. */ challenging: EntryWave[][]; introSeconds: number; clearSeconds: number; resultSeconds: number; }— Stage sequence: entries, challenging stages and their timings.entrySpeed: Ramp— Entry flight speed by stage (px/s).attack: { speed: Ramp; /** Seconds between dive launches. */ interval: Ramp; /** Simultaneous dive groups. */ maxDivers: Ramp; /** Relative chance a dive picks each kind. */ weights: Record<string, number>; /** Once this few remain the attack quickens: half the interval and one more simultaneous dive. */ frenzyAtOrBelow: number; /** Dives start only after the whole fleet has arrived. */ afterEntry: boolean; /** Speed while flying home to a slot. */ homeSpeed: number; }— Dive selection, timing and speed rules.capture: { program: string; /** Chance a capture-capable kind's dive is a capture dive when a capture is allowed. */ chance: Ramp; beamWidth: number; /** Beam reach below the boss. */ beamLength: number; /** Opening, holding and closing seconds. */ beamOpen: number; beamHold: number; beamClose: number; pullSeconds: number; /** The captured ship's enemy kind (scored when shot). */ fighterKind: string; /** Program the hostile fighter dives with after its boss died in formation. */ hostileDive: string; } | null— Tractor-beam capture rules, or null for none.fire: { speed: Ramp; maxActive: number; /** Shots per dive. */ perDive: Ramp; /** Seconds between a diver's shots. */ gap: number; /** Only fire between these heights. */ minY: number; maxY: number; /** Largest angle from straight down. */ maxAngle: number; /** Chance an entering enemy fires once. */ entryChance: Ramp; width: number; height: number; }— Enemy fire rules.player: { y: number; width: number; height: number; speed: number; minX: number; maxX: number; lives: number; extraLives: number[]; /** After the last listed threshold, another every this many points (0 = none). */ extraEvery: number; /** Missiles in flight for a single fighter; a dual fighter fires pairs up to twice this. Original: 2. */ maxShots: number; shotSpeed: number; shotWidth: number; shotHeight: number; dualSpacing: number; requireRelease: boolean; deathSeconds: number; }— Fighter movement, missiles, lives and extra lives.challengeScoring: { perHit: number; perfect: number; /** Bonus for destroying a whole group, by challenging-stage index (last repeats). */ groupBonus: number[]; }— Challenging-stage bonuses.
GalagaEnemy
interface GalagaEnemy
One enemy (or captured fighter) with its flight state.
id: string— Stable id (slot id in normal stages).kind: string— Kind id.slot: string | null— Formation slot, or null for challenging flyers.slotOffsetY: number— Extra formation offset (a hostile fighter sits above its old boss).hp: number— Remaining hits.phase: GalagaPhase— Current phase.x: number— Centre x.y: number— Centre y.heading: number— Heading in degrees (0 right, 90 down).program: string— Flight program being flown.step: number— Index of the current program step.stepTime: number— Seconds into the current step.mirror: boolean— Flying the mirrored program.entering: boolean— Flying an entry route (scores diving points, cannot be selected to attack).diving: boolean— Part of a dive: scores its diving value.spawnAt: number— Stage time at which it enters.wave: number— Entry group index (-1 for a captured fighter).leader: string | null— Boss this escort follows, if any.offsetX: number— Escort offset from its leader.offsetY: number— Escort offset from its leader.escortsLaunched: number— Escorts that left with this boss.escortsKilled: number— Of those, destroyed before this boss.captive: string | null— Id of a captured fighter riding with this enemy.rider: string | null— Rider/puller: the boss this fighter belongs to.beamTime: number— Seconds into the tractor beam.fireLeft: number— Shots still to fire on this flight.fireClock: number— Seconds until the next shot may fire.animTime: number— Seconds of the current pull/rescue animation.exit: boolean— While leaving: true = gone for good off screen, false = wrap back in from the top.speed: number— Base flight speed for this flight (px/s).
GalagaEvent
type GalagaEvent = | { kind: "stage-start"; stage: number; challenging: boolean } | { kind: "player-fire"; x: number; y: number } | { kind: "enemy-hit"; id: string; enemy: string; x: number; y: number } | { kind: "enemy-killed"; id: string; enemy: string; score: number; x: number; y: number; diving: boolean } | { kind: "dive"; id: string; enemy: string; escorts: string[]; capture: boolean } | { kind: "beam"; id: string } | { kind: "captured"; boss: string; fighter: string } | { kind: "captive-hostile"; fighter: string } | { kind: "rescued"; fighter: string } | { kind: "player-hit"; x: number; y: number; dual: boolean } | { kind: "player-ready" } | { kind: "extra-life"; lives: number } | { kind: "group-bonus"; score: number; x: number; y: number } | { kind: "stage-clear"; stage: number } | { kind: "challenge-result"; hits: number; total: number; bonus: number; perfect: boolean } | { kind: "game-over" }
Ordered notifications for presentation: drain() them each frame.
GalagaInput
interface GalagaInput
One step of player input.
move?: number— -1 left, 0 still, +1 right.fire?: boolean— Fire held.
GalagaKind
interface GalagaKind
An enemy type.
id: string— Kind id referenced by slots, waves and weights.formationScore: number— Score when destroyed in formation, and while flying (entering, diving or returning).divingScore: number— Score when destroyed while flying.escortScores?: number[]— Diving score by number of its escorts destroyed earlier in the same dive (boss: 400/800/1600).hp: number— Hits to destroy (Boss Galaga: 2).width: number— Hit-box width.height: number— Hit-box height.dives: string[]— Dive program names; one is chosen per dive (seeded).loopDives?: string[]— Dive programs used fromloopFromStageon (bees start looping).loopFromStage?: number— First stage that may useloopDives.escorts?: { kind: string; max: number }— Escort kind and count this kind takes on a dive (boss: two butterflies).captures?: boolean— This kind may carry out capture dives.
GalagaPhase
type GalagaPhase = | "waiting" | "flying" | "leaving" | "homing" | "formation" | "beaming" | "pulled" | "riding" | "rescuing" | "dead" | "gone"
Where an enemy is in its life: waiting to enter, flying a program, leaving the screen, flying home, in formation, beaming, a captured ship being pulled up, riding its boss, a rescued ship descending, dead or gone.
GalagaPresetOptions
interface GalagaPresetOptions
Options for galagaPreset.
rows?: GalagaRow[]— Custom rows, top first (default: 4 bosses, 2x8 butterflies, 2x10 bees).groupSize?: number— Members per entry group for custom rows (default 8).lives?: number— Starting fighters, including the one in play (3 = classic).capture?: boolean— Boss tractor beams and captured-fighter rescue.challengingFirst?: number | null— First challenging stage (null for none) and the interval after it.challengingEvery?: number— Stages between challenging stages after the first (4 = classic).aggression?: number— Multiplies dive frequency and simultaneous divers (1 = classic).maxShots?: number— Simultaneous single-fighter missiles (2 = classic).extraLives?: number[]— Scores that award an extra fighter (classic 20,000 and 70,000).extraEvery?: number— After the listed scores, another fighter every this many points (classic 70,000; 0 = none).seed?: number— Seed for dive selection and capture chance.
GalagaRow
interface GalagaRow
A formation row: count enemies of kind, centred on the grid.
kind: string— Enemy kind id for every ship in the row.count: number— Ships in the row.
GalagaShot
interface GalagaShot
A missile or enemy bullet.
id: number— Stable id for drawing.x: number— Centre x.y: number— Top y for missiles, centre y for bullets.vx: number— Velocity x (px/s).vy: number— Velocity y (px/s).
GalagaSlot
interface GalagaSlot
A formation slot on the grid.
id: string— Unique slot id; also the enemy's id in normal stages.kind: string— Kind that fills the slot.col: number— Grid column (fractions allowed for centred rows).row: number— Grid row, 0 at the top.
GalagaState
interface GalagaState
Serializable game state; read it through state.
time: number— Seconds since the game began.stage: number— Stage number, from 1.challenging: boolean— This stage is a challenging stage.challengeIndex: number— Challenging stages begun so far, minus one.status: GalagaStatus— Game status.statusTime: number— Seconds in the current status.stageTime: number— Seconds of play in this stage.score: number— Score.lives: number— Fighters left, including the one in play.extraAwarded: number— Extra fighters awarded so far.player: { x: number; alive: boolean; dual: boolean; deadTime: number; captured: boolean; pendingDual: boolean; }— The fighter: x, alive, dual, death timer, captured and a rescue waiting for respawn.enemies: GalagaEnemy[]— Every enemy of the stage, and any captured fighter.playerShots: GalagaShot[]— Missiles in flight.enemyShots: GalagaShot[]— Enemy bullets in flight.fireHeld: boolean— Fire was held on the last step (release rule).entryDone: boolean— Every entering enemy has arrived or died.breatheTime: number— Seconds of breathing.attackClock: number— Seconds since the last dive launch.side: number— Side of the next dive (-1 left, 1 right).hits: number— Challenging-stage hits.total: number— Enemies in this stage.groupHits: number[]— Hits per entry group.groupSizes: number[]— Members per entry group.lastBonus: number— Last challenging-stage bonus.perfect: boolean— The last challenging stage was perfect.nextId: number— Next shot/captive id.random: number[]— The kernel generator's state (four words fromcreateRng), advanced in place, so a snapshot carries the stream.
GalagaStatus
type GalagaStatus = | "intro" | "playing" | "clear" | "result" | "game-over"
Stage intro banner, play, cleared, challenging-stage result, or game over.
Ramp
interface Ramp
A value that changes with the stage: base + perStage * (stage - 1), clamped to min/max.
base: number— Value at stage 1.perStage?: number— Change per stage after the first.min?: number— Lower clamp.max?: number— Upper clamp.


