The pack
marchingFleet
function marchingFleet(): Pack<{ marchingFleet: MarchingFleetApi }>
Install the renderer-independent marching-fleet rules (Space Invaders). Its shields are destructible-cover masks.
Helpers
createInvaders
function createInvaders(config: InvadersConfig)
Create an independent marching-fleet game. No renderer or kernel clock is required.
invadersPreset
function invadersPreset(o: InvadersPresetOptions = {}): InvadersConfig
Space Invaders (Taito, 1978) as data. Values from the ROM disassembly (computerarcheology.com): 224x256 screen; 11x5 rack of squid (30), crab (20), octopus (10) on 16-pixel centres; one invader moved per frame, 2 px (the last invader 3 px right, 2 left), 8 px drop at an edge; rounds start lower from the AlienStartTable; one player shot at 4 px/frame that needs the button released; rolling (aimed at the column over the cannon), plunger and squiggly shots from the ColFireTable, each stepping 4 px every third frame (5 px with 8 or fewer invaders), reloading by score; the saucer every 1536 frames with 8+ invaders, its score from the 15-entry wrap of SaucerScrTab; four 22x16 shields eroded by the ROM's explosion stencils; 3 cannons and one extra at 1500.
Classes
Invaders
class Invaders
Owns a marching fleet game: rack, cursor march, shot types, saucer, eroding cover, lives and rounds.
readonly config: InvadersConfig— The validated definition (a private copy).readonly cover: DestructibleCover— The shields, a destructible-cover instance owned by this game.constructor(config: InvadersConfig)get state(): InvadersState— A read-only copy of the whole state.box(inv: InvaderState)— Live invaders' hit boxes, for renderers and tests.reloadSteps(score = this.s.score)— Current reload threshold (steps) for the score.saucerValue()— Score the saucer would award if hit now.tick(seconds: number, input: InvadersInput = {})— Advance by real seconds (fixed frames inside); returns the state.step(input: InvadersInput = {})— Advance exactly one simulation frame.drain()— Take the events since the last drain.snapshot()— Serializable snapshot of the whole game.restore(snap: ReturnType<Invaders["snapshot"]>)— Restore a snapshot taken from the same configuration.reset(round = 0)— Start a new game with the same configuration, optionally at a later (0-based) round.
Types
InvaderKind
interface InvaderKind
One invader type: hitbox (centred in its fleet cell) and score.
id: string— Kind id used by fleet rows.score: number— Points for shooting one.width: number— Hit-box width.height: number— Hit-box height.
InvadersConfig
interface InvadersConfig
Complete marching-fleet definition. Every original value is a preset in invadersPreset.
width: number— Playfield width in model pixels.height: number— Playfield height in model pixels.hz: number— Simulation frames per second.seed?: number— Seed for random-column shots.kinds: InvaderKind[]— Invader kinds.fleet: { columns: number; /** Kind id per row, TOP row first. */ rows: string[]; /** Cell size; invaders' hitboxes are centred in their cell. */ cellWidth: number; cellHeight: number; /** Distance between column / row origins. */ spacingX: number; spacingY: number; /** Left edge of column 0 at the start of a round. */ left: number; /** Top of the BOTTOM row per round; the last entry repeats from \repeatFrom`. */ startY: number[]; repeatFrom?: number; }` — Rack layout and the start height of each round.march: { /** Invaders moved per frame (original 1: one invader per frame). */ movesPerFrame: number; stepX: number; /** Horizontal steps of the very last invader, right and left (original 3 and 2). */ lastStepRight: number; lastStepLeft: number; /** Drop applied in the sweep after the rack touches an edge. */ stepDown: number; /** Playfield edges that reverse the rack. */ minX: number; maxX: number; /** Frames the rack freezes while a shot invader explodes. Original: 16. */ killPause: number; /** Invaders erase any cover pixels they overlap (original behaviour). */ eraseCover: boolean; }— Cursor march rules.invasionY: number— An invader's bottom reaching this line lands the invasion: game over regardless of lives.player: { /** Top of the cannon. */ y: number; width: number; height: number; /** Pixels per frame. */ speed: number; minX: number; maxX: number; startX: number; lives: number; /** Extra cannons awarded at these scores (once each). Original: 1500. */ extraLives: number[]; /** Frames of the death explosion, during which the rack and its fire freeze. */ deathFrames: number; /** Frames before the new cannon appears after the explosion. */ respawnFrames: number; }— Cannon rules.playerShot: { /** Pixels per frame. Original: 4. */ speed: number; /** Simultaneous player shots. Original: 1. */ maxActive: number; /** Fire must be released between shots (original). */ requireRelease: boolean; /** The shot explodes here (top of the playfield). */ ceiling: number; width: number; height: number; /** Player shots can destroy alien shots they touch (original). */ hitsAlienShots: boolean; /** Frames the shot's explosion lingers before another shot may fire. */ explodeFrames: number; }— Player shot rules.alienShots: { types: InvaderShotType[]; /** Each shot type takes a turn every \cadence` frames, offset by its index. Original: 3. / cadence: number; /* Pixels per step, and the faster step once `fastAtOrBelow` invaders remain. Original: 4, 5 and 8. / step: number; fastStep: number; fastAtOrBelow: number; /* * Reload: a type may fire only when every other in-flight shot has taken MORE than this many steps. * Looked up by score: `rates[i]` applies while score < `scores[i]`, the last rate beyond. * Original: scores 200/1000/2000/3000, rates 48/16/11/8/7. / reload: { scores: number[]; rates: number[] }; /* Frames at the start of each life/round before aliens may fire. / initialDelay: number; /* Shots are lost below this line (the ground). */ floor: number; }` — Alien shot types, cadence, speed and reload.saucer: SaucerRules | null— Mystery ship rules, or null for none.cover: CoverDefinition[]— Damageable shields (destructible-cover definitions).splats: { player: string[] | null; alien: string[] | null; inset: number }— Explosion stencils (null = destroy the single contacted cell), centredinsetpixels into the cover.roundDelay: number— Frames between clearing a round and the next rack.restoreCover: boolean— Cover restored for each new round (original).
InvadersEvent
type InvadersEvent = | { kind: "player-fire"; x: number; y: number } | { kind: "invader-killed"; id: string; invader: string; score: number; x: number; y: number } | { kind: "saucer-start"; dir: number } | { kind: "saucer-killed"; score: number; x: number; y: number } | { kind: "saucer-gone" } | { kind: "shot-exploded"; team: "player" | "alien"; x: number; y: number; on: "cover" | "ceiling" | "floor" | "shot" } | { kind: "player-hit"; x: number; y: number } | { kind: "extra-life"; lives: number } | { kind: "march"; sweep: number } | { kind: "round-clear"; round: number } | { kind: "round-start"; round: number } | { kind: "game-over"; reason: "lives" | "invasion" }
Ordered notifications for presentation: drain() them each frame.
InvaderShot
interface InvaderShot
One alien shot in flight.
id: number— Stable id for drawing.type: string— Its shot type id.x: number— Centre x.y: number— Top y.steps: number— Steps taken (reload bookkeeping and animation).active: boolean— In flight.
InvaderShotSelect
type InvaderShotSelect = /** The lowest live invader in the column over the player's centre (the "rolling" shot). */ | { kind: "player-column" } /** The lowest live invader in the next column of a looping 1-based column table. */ | { kind: "column-table"; table: number[]; start?: number } /** The lowest live invader in a seeded random column (not in the original; a variant policy). */ | { kind: "random" }
How an alien shot type picks the invader that drops it.
InvaderShotType
interface InvaderShotType
One alien shot type. At most one shot of each type is in flight, so N types cap alien fire at N shots.
id: string— Shot type id (also its drawing key).select: InvaderShotSelect— Which invader drops it.disableAtOrBelow?: number— Do not fire this type while this many invaders or fewer remain (the plunger stops at 1).sharesSaucerSlot?: boolean— This type's turn is shared with the saucer, which waits for it to land (the squiggly shot).width?: number— Hit-box width (default 3).height?: number— Hit-box height (default 7).
InvadersInput
interface InvadersInput
One frame of player input.
move?: number— -1 left, 0 still, +1 right.fire?: boolean— Fire held.
InvadersPresetOptions
interface InvadersPresetOptions
Options for invadersPreset: the classic values unless overridden.
columns?: number— Invaders per row (11 = classic).rows?: string[]— Kind ids per row, top first. Default: squid, crab, crab, octopus, octopus.lives?: number— Starting cannons (3 = classic).bunkers?: number— Shields (4 = classic, at the original positions).saucer?: boolean— Include the mystery ship (default true).movesPerFrame?: number— Invaders moved per frame (1 = original; raise for a faster, smoother march).playerShots?: number— Simultaneous player shots (1 = original).shotTypes?: ("rolling" | "plunger" | "squiggly")[]— Which original shot types to use.extraLives?: number[]— Scores awarding an extra cannon ([1500] = classic).seed?: number— Seed for random-column shots.
InvadersState
interface InvadersState
Serializable game state; read it through state.
frame: number— Frames since the game began.round: number— Rack number, from 0.score: number— Score.lives: number— Cannons left, including the one in play.extraAwarded: number— Extra cannons awarded so far.status: InvadersStatus— Game status.gameOverReason: "" | "lives" | "invasion"— Why the game ended.statusFrames: number— Frames left in the current status.player: { x: number; alive: boolean; visible: boolean }— The cannon: left x, alive and visible.invaders: InvaderState[]— Every invader in march order (bottom row first).alive: number— Invaders left.cursor: number— Cursor into the bottom-up march order; direction +1 right, -1 left.dir: number— Rack direction: +1 right, -1 left.sweepDown: number— Drop applied during the current sweep.bumped: boolean— A bump was detected during this sweep: next sweep drops and reverses.killPause: number— Frames the rack stays frozen for an explosion.sweeps: number— Total completed march sweeps (presentation: the four-note fleet sound).shots: InvaderShot[]— Alien shots in flight.playerShots: PlayerShotState[]— Player shots in flight.shotsFired: number— Player shots fired this game (drives the saucer table and direction).fireHeld: boolean— Fire was held on the last frame (release rule).shotLock: number— Frames before another player shot may fire.fireDelay: number— Frames before aliens may fire.tablePointers: Record<string, number>— Column-table position per shot type.saucer: SaucerState— Mystery ship.nextId: number— Next shot id.random: number[]— The kernel generator's state (four words fromcreateRng), advanced in place, so a snapshot carries the stream.
InvadersStatus
type InvadersStatus = | "playing" | "player-dying" | "round-clear" | "game-over"
Playing, the cannon exploding, between racks, or over.
InvaderState
interface InvaderState
One invader.
id: string— Stable idr<row>c<col>.kind: string— Kind id.col: number— Fleet column.row: number— Fleet row, 0 at the top.x: number— Top-left of the invader's cell.y: number— Top of its cell.alive: boolean— Not yet shot.frame: number— Two-frame march animation; flips each time this invader moves.
MarchingFleetApi
type MarchingFleetApi = typeof invaders
game.marchingFleet: the marching-fleet rules owner and its preset.
PlayerShotState
interface PlayerShotState
One player shot in flight.
id: number— Stable id for drawing.x: number— Centre x.y: number— Top y.
SaucerRules
interface SaucerRules
The mystery ship.
interval: number— Frames between saucer launches. Original: 0x600 = 1536 (25.6 s).minInvaders: number— No saucer when fewer invaders remain. Original: 8.y: number— Top of the saucer's 16x8 box.width: number— Saucer width.height: number— Saucer height.step: number— Pixels per step; the saucer steps on its shared shot turn. Original: 2.scores: number[]— Score table indexed by player shots fired, wrapping afterwrapentries.wrap: number— Table entries used before the index wraps (original: 15 of 16, a ROM quirk).directionByShots: boolean— Odd shot count → enter from the left, even → from the right (original). Otherwise alternate.timerBelowY?: number— The saucer timer only runs while some live invader's top is below this y (original: the rack has dropped since round 1's start).scoreFrames: number— Frames the score is shown after a hit (presentation hint and saucer-slot lockout).
SaucerState
interface SaucerState
Mystery ship state.
active: boolean— Crossing now.x: number— Left edge.dir: number— +1 moving right, -1 left.timer: number— Frames since the last saucer left or was hit.shown: number— Score shown after a hit, counting downshowFrames.showFrames: number— Frames the shown score lasts.showX: number— Where the score is shown.


