The pack
missileDefence
function missileDefence(): Pack<{ missileDefence: typeof api }>
Install the renderer-independent missile defence rules (Missile Command and its variants) as game.missileDefence.
Helpers
createMissileDefence
function createMissileDefence(config: MissileDefenceConfig)
Create an independent missile defence game. No renderer or kernel clock is required.
explosionRadii
function explosionRadii(o: { radius: number; grow: number; hold?: number; shrink: number }): number[]
Build an explosion radius table: grow to radius over grow steps, hold hold steps, shrink over shrink.
MISSILE_COMMAND_EXPLOSION
const MISSILE_COMMAND_EXPLOSION: ExplosionDef
The arcade's explosion radius table (ROM $54B7): 0, 0, 2..13, 13..1, 0, stepped every 5 frames.
missileCommandPreset
function missileCommandPreset(o: MissileCommandPresetOptions = {}): MissileDefenceConfig
Missile Command (Atari, 1980) as data. Values from the annotated Rev 3 ROM disassembly (6502disassembly.com): a 256 x 231 field; bases Alpha/Delta/Omega at x 20/123/240 with 10 ABMs each per wave, side ABMs 3 units/frame and the centre's 7; at most 8 ABMs in flight; six cities at the ROM's positions; up to 8 attack slots (smart bombs cost two, at most three), launched four at a time once the highest is below altitude 202 - 2 x wave (at least 180); MIRVs split an ICBM in the 128-159 altitude band into up to three more; fliers from wave 2 (bombers 1/3, satellites 1/2 unit/frame) firing between x 48 and 208; octagonal explosions stepping the ROM radius table every 5 frames, chaining, never testing below altitude 33; 25/125/100 points, 5 per ABM and 100 per city left, multiplier 1x-6x every two waves; at most 3 cities lost per wave; a bonus city every 10,000.
missileCommandWaves
function missileCommandWaves(height = 231): WaveDef[]
The arcade's wave table (ROM $6090 ICBM counts, $60A3/$60B2 move delays, $60C1 smart bombs, flier tables $60D4/$60DB and the altitude bands), converted to y-down units on the 231-line field.
octagonDistance
function octagonDistance(dx: number, dy: number): number
The arcade's octagonal distance: max(|dx|, |dy|) + 3/8 min(|dx|, |dy|).
octagonPoints
function octagonPoints(x: number, y: number, r: number): MdPoint[]
The outline of an octagonal explosion of radius r (eight points, clockwise from +x), for drawing.
rampValue
function rampValue(start: number, step: number, limit: number, wave: number): number
Linear ramp over waves: start + step * (wave - 1), clamped to limit (an upper limit when step > 0, lower when < 0).
rampWaves
function rampWaves(o: RampWavesOptions): WaveDef[]
Generate a wave table from linear ramps: a quick way to author a difficulty curve for a variant.
validateMissileDefence
function validateMissileDefence(c: MissileDefenceConfig)
Throw a clear error for an unsupported definition. Called by the constructor.
Classes
MissileDefence
class MissileDefence
Owns a missile defence game: batteries, targets, incoming waves, explosions, scoring, bonus targets and the tally.
readonly config: MissileDefenceConfig— The validated definition (a private copy).constructor(config: MissileDefenceConfig)get state(): MissileDefenceState— Live state (read only).reset(wave = 1)— Start a new game atwave(default 1).waveDef(wave = this.s.wave): WaveDef— The wave definition used forwave(default the current one).multiplier(wave = this.s?.wave ?? 1): number— The score multiplier forwave(default the current one).get ammo(): number— ABMs left across all standing batteries.get slotsUsed(): number— Attack slots in use (smart bombs countslotCost).clampAim(p: MdPoint): MdPoint— Clamp a point to the crosshair limits.aim(p: MdPoint)— Put the crosshair at a point (clamped).pickBattery(order: FireOrder = "auto", at: MdPoint = this.s.cursor): { battery: number } | { reason: FireRefusal; battery: number | null }— The battery index an order resolves to for a launch atat, or a refusal.fire(order: FireOrder = "auto", at?: MdPoint): FireResult— Launch an ABM at the crosshair (orat) now. Only while the attack is on.tick(seconds: number, input: MissileDefenceInput = {})— Advance by real seconds (fixed frames inside); aim and fire orders apply once, first. Returns the state.step(input: MissileDefenceInput = {})— Advance exactly one simulation frame.predict(attack: AttackState, frames: number): MdPoint— Where an attack will be afterframesframes on its current heading (straight-line estimate).interceptPoint(attack: AttackState, battery: number, lead = 0): { x: number; y: number; frames: number } | null— Aim point for batterybatteryto meetattackleadframes after detonation (null if outside the crosshair limits).chooseShot(o: { lead?: number; cover?: number } = {}): DefenceShot | null— A competent defender: the lowest attack not already covered by an ABM or a growing explosion, met by whichever battery detonates soonest, aimed so the attack arrives as the blast grows. Null = hold fire.drain()— Take the events since the last drain.snapshot()— Serializable snapshot of the whole game.restore(snap: ReturnType<MissileDefence["snapshot"]>)— Restore a snapshot taken from the same configuration.
Types
AbmState
interface AbmState
One ABM in flight.
id: number— Stable id.battery: string— Launching battery id.ox: number— Launch point (the trail starts here).oy: number— Launch point y in model units (y down).x: number— Head.y: number— Head y in model units (y down).tx: number— Detonation point (the arcade's flashing X).ty: number— Detonation point y in model units (y down).speed: number— Units per frame.
AttackKind
type AttackKind = "icbm" | "smart"
An ICBM or a smart bomb.
AttackState
interface AttackState
One incoming attack.
id: number— Stable id.kind: AttackKind— ICBM or smart bomb.ox: number— Where it was launched (the trail starts here).oy: number— Launch point y in model units (y down).x: number— Head.y: number— Head y in model units (y down).tx: number— Aim point.ty: number— Aim point y in model units (y down).target: string | null— Target or battery id it is aimed at, or null for open ground.source: "top" | "mirv" | "flier"— Launched from the top, split from a MIRV, or dropped by a flier.canSplit: boolean— May still split.evading: boolean— A smart bomb dodging this frame.
BatteryDef
interface BatteryDef
One ABM launcher (an arcade "base" or silo).
id: string— Stable id, also used as a fire order. Must not be"auto".x: number— Centre x.y: number— Ground level at the battery: an ICBM aimed at it lands here.top: number— Launch height: ABMs leave from (x, top).ammo: number— ABMs per wave. Original: 10 at each of three bases.speed: number— ABM speed in units per frame. Original: 3 at the side bases, 7 at the centre.explosion?: ExplosionDef— This battery's explosion, overridingexplosions.abm.
BatteryState
interface BatteryState
One ABM battery now.
id: string— Battery id.x: number— Centre x.y: number— Ground y.top: number— Launch y.ammo: number— ABMs left.alive: boolean— Not destroyed this wave.
DefenceShot
interface DefenceShot
A shot the defence bot recommends.
x: number— Aim point (inside the crosshair limits).y: number— Aim point y in model units (y down), inside the crosshair limits.battery: number— Battery index.attack: number— Attack (or -1 for the flier) it is meant for.frames: number— Frames until the ABM detonates.
ExplosionDef
interface ExplosionDef
An explosion: a radius table stepped every framesPerStep frames. It ends after the last entry.
radii: number[]— Radius per step. Original: 0, 0, 2..13, 13..0 (28 entries, max 13).framesPerStep: number— Frames between steps (and between kill tests). Original: 5.
ExplosionState
interface ExplosionState
One explosion.
id: number— Stable id.x: number— Centre.y: number— Centre y in model units (y down).radius: number— Current radius.step: number— Index into the radius table.age: number— Frames since it began.table: number— -1 = the attack explosion, otherwise the battery index whose ABM made it.source: "abm" | "attack" | "impact" | "flier"— What exploded.
FireOrder
type FireOrder = number | string
A fire order: a battery index, a battery id, or "auto" for the launch policy.
FireRefusal
type FireRefusal = "not-playing" | "no-ammo" | "destroyed" | "in-flight-limit" | "unknown-battery"
Why a fire order did nothing.
FireResult
type FireResult = { ok: true; battery: string; abm: number } | { ok: false; reason: FireRefusal; battery: string | null }
Result of a fire order.
FlierKind
interface FlierKind
One kind of flier (bomber, killer satellite ...).
id: string— Kind id.speed: number— Units per frame. Original: bomber 1/3, satellite 1/2.score: number— Points (times the multiplier). Original: 100.hitSize: number— Added to an explosion's radius for the kill test. Original: 6.weight: number— Relative chance of this kind.
FlierState
interface FlierState
The flier crossing now.
kind: string— Kind id.x: number— Centre.y: number— Centre y in model units (y down).dir: number— +1 right, -1 left.fireTimer: number— Frames before it may fire.
FlierWave
interface FlierWave
Flier rules for one wave.
minY: number— Altitude band the flier crosses at (y range).maxY: number— Largest y of the band in model units (y down); each flier crosses at a random y fromminYto this. Must be at leastminY.cooldown: number— Frames after a flier leaves or dies before the next appears.fireDelay: number— Frames between a flier's volleys (the first volley also waits this long).burst: number— ICBMs per volley, each to a different target (a volley is a split from the flier: original up to 3). It still needs free slots and the wave budget.
MdPoint
type MdPoint = { … }
A point in model units, y down.
x: numbery: number
MissileCommandPresetOptions
interface MissileCommandPresetOptions
Options for missileCommandPreset: the classic values unless overridden.
cities?: number— Cities (6 = classic, at the ROM positions; other counts are spread evenly between the bases).ammo?: number— ABMs per base per wave (10 = classic).bonusCityEvery?: number | null— Bonus city every this many points (10,000 = classic default DIP; 8,000-20,000 or null = none).mirvChance?: number— Chance an eligible launch splits a MIRV instead of launching from the top (1 = the arcade).maxCityLossPerWave?: number | null— Cities lost per wave at most (3 = classic; null = no protection).seed?: number— Seed.
MissileDefenceConfig
interface MissileDefenceConfig
Complete missile defence definition. Every original value is a preset in missileCommandPreset.
width: number— Playfield width in model units.height: number— Playfield height in model units.hz: number— Simulation frames per second.seed?: number— Seed for spawn positions, targets, MIRVs, fliers and bonus rebuilds.aim: { minX: number; maxX: number; minY: number; maxY: number; speed: number; startX: number; startY: number }— The crosshair: limits and keyboard speed (units per frame at full deflection).batteries: BatteryDef[]— ABM batteries, in fire-button order.targets: TargetDef[]— Protected targets.launch: { /** Which battery an \"auto"` order uses: the nearest with ammo, or the one whose ABM arrives soonest. / policy: "nearest" | "fastest"; /* ABMs in flight at once. Original: 8. / maxInFlight: number; /* A battery reports `low-ammo` when a launch leaves this many or fewer. Original: 3 ("LOW"). */ lowAt: number; }` — ABM launch rules.attacks: { /** Where top launches start. Original: altitude 222, any x. */ spawnY: number; spawnMinX: number; spawnMaxX: number; /** Attack slots in flight (a smart bomb costs \smartBomb.slotCost`). Original: 8. / maxActive: number; /* Attacks launched at once from the top. Original: 4. / perBatch: number; /* Plain ICBMs. / icbm: { score: number; hitSize: number }; /* Smart bombs: no trail, dodge explosions, never change target. / smartBomb: { score: number; hitSize: number; /* Attack slots each costs. Original: 2. / slotCost: number; /* At most this many in flight. Original: 3. / maxActive: number; /* Chance a launch is a smart bomb while ICBMs remain (afterwards every launch is). Original: 1/4. / launchChance: number; /* No smart bomb launches while this many attacks are in flight. Original: 5. / crowdLimit: number; /* Distance of the eight explosion probes. / scan: number; /* From this wave a bomb may dodge upward. Original: 9. / evadeUpFromWave: number; }; /* MIRV splitting, or null for none. / mirv: { /* First wave with splits. Original: 1 (the ROM's wave-1 test never fires). / fromWave: number; /* The split band, as y. Original: altitude 128-159. / minY: number; maxY: number; /* * How the candidate is found. `"running-max"` is the ROM: walking the attacks in slot order, an ICBM is a * candidate whenever the highest attack seen so far is in the band, whatever its own height. * `"in-band"`: the candidate itself must be in the band. / candidate: "running-max" | "in-band"; /* Each ICBM splits at most once (the ROM allows repeats). / once: boolean; /* New missiles per split, each to a different target. Original: up to 3. / maxChildren: number; /* Chance an eligible launch splits instead of launching from the top. Original: 1. / chance: number; /* No split while this many explosions are alive, or null. Original: 12. / explosionLimit: number | null; } | null; /* Explosions of destroyed or landed attacks hold an attack slot until they end. Original: true. / explosionsHoldSlots: boolean; /* Flier kinds and where they may fire. Original: bombers and satellites, firing between x 48 and 208. */ fliers: { kinds: FlierKind[]; fireMinX: number; fireMaxX: number }; }` — Incoming attack rules.explosions: { /** Default ABM explosion (a battery may override it). */ abm: ExplosionDef; /** Explosions of destroyed attacks and impacts. */ attack: ExplosionDef; /** Kill test shape: the arcade's octagon (max + 3/8 min) or a circle. */ shape: "octagon" | "circle"; /** Explosion slots; the oldest is replaced when full. Original: 20. */ max: number; /** A destroyed attack or flier explodes too, so kills chain. Original: true. */ chain: boolean; /** Attacks lower than this y are never tested (the ground band). Original: altitude 33. */ safeBelowY: number; }— Explosion rules.waves: WaveDef[]— Wave table; the last entry repeats.multipliers: number[]— Score multiplier per wave; the last entry repeats. Original: 1,1,2,2,3,3,4,4,5,5,6.scoring: { /** Points (times the multiplier) per ABM left at the end of a wave. Original: 5. */ abmBonus: number; /** A bonus target is banked every this many points, or null for none. Original: 10,000. */ bonusTargetEvery: number | null; }— Bonus scoring.rules: { /** At most this many targets can be lost in one wave (the attack retargets batteries), or null for no cap. Original: 3. */ maxTargetLossPerWave: number | null; /** End the wave at once when no ABMs are left and nothing more can be lost (cap reached or no targets). Original: true. */ endWaveWhenDefenceless: boolean; /** End the wave (and so the game) the moment the last target falls with no bonus banked. Original: false. */ endWaveWhenNoTargets: boolean; /** Ammunition each new wave: refill to each battery's \ammo`, or add to what is left up to a cap. Original: refill. / replenish: "refill" | { add: number; max: number }; /* Destroyed batteries are rebuilt for each wave. Original: true. */ restoreBatteries: boolean; }` — Wave and survival rules.timing: { readyFrames: number; abmTallyFrames: number; targetTallyFrames: number; tallyPauseFrames: number }— Frame counts of the wave intro and the end-of-wave tally.
MissileDefenceEvent
type MissileDefenceEvent = | { kind: "wave-start"; wave: number; multiplier: number } | { kind: "attack-start"; wave: number } | { kind: "abm-launch"; battery: string; x: number; y: number; tx: number; ty: number } | { kind: "fire-refused"; reason: FireRefusal; battery: string | null } | { kind: "low-ammo"; battery: string; ammo: number } | { kind: "battery-empty"; battery: string } | { kind: "explosion"; id: number; x: number; y: number; source: ExplosionState["source"] } | { kind: "attack-launch"; id: number; attack: AttackKind; source: AttackState["source"]; x: number; y: number } | { kind: "mirv"; parent: number; children: number[]; x: number; y: number } | { kind: "attack-destroyed"; id: number; attack: AttackKind; score: number; x: number; y: number } | { kind: "impact"; id: number; attack: AttackKind; x: number; y: number; target: string | null } | { kind: "target-destroyed"; target: string; x: number } | { kind: "battery-destroyed"; battery: string; x: number } | { kind: "flier-start"; flier: string; dir: number; y: number } | { kind: "flier-fire"; flier: string; count: number; x: number; y: number } | { kind: "flier-destroyed"; flier: string; score: number; x: number; y: number } | { kind: "flier-gone"; flier: string } | { kind: "wave-clear"; wave: number; abandoned: boolean } | { kind: "tally-abm"; battery: string; points: number } | { kind: "tally-target"; target: string; points: number } | { kind: "bonus-earned"; bank: number } | { kind: "target-rebuilt"; target: string } | { kind: "game-over"; wave: number; score: number }
Ordered notifications for presentation: drain() them each frame.
MissileDefenceInput
interface MissileDefenceInput
One tick or frame of player input.
aim?: MdPoint— Put the crosshair here (pointer).move?: MdPoint— Move the crosshair: -1..1 per axis, timesaim.speedunits per frame (keys, stick).fire?: FireOrder[]— Fire orders, applied once before the frames run.
MissileDefenceState
interface MissileDefenceState
Serializable game state; read it through state and do not mutate it.
frame: number— Frames since the game began.wave: number— Wave number, from 1.score: number— Score.status: MissileDefenceStatus— Game status.statusFrames: number— Frames left inready/tally steps; frames since THE END ingame-over.multiplier: number— This wave's multiplier.cursor: MdPoint— The crosshair.batteries: BatteryState[]— Batteries, in config order.targets: TargetState[]— Targets, in config order.abms: AbmState[]— ABMs in flight.attacks: AttackState[]— Attacks in flight.explosions: ExplosionState[]— Explosions.flier: FlierState | null— The flier, if one is crossing.flierCooldown: number— Frames before the next flier.remaining: { icbms: number; smartBombs: number }— Attacks this wave not yet launched.lostThisWave: number— Targets lost this wave.bonusBank: number— Bonus targets banked and not yet rebuilt.bonusAwarded: number— Bonus thresholds passed so far.tally: TallyState— Tally progress (meaningful intally).nextId: number— Next entity id.random: number[]— The kernel generator's state (four words fromcreateRng), advanced in place, so a snapshot carries the stream.
MissileDefenceStatus
type MissileDefenceStatus = "ready" | "playing" | "wave-end" | "tally" | "game-over"
Wave intro, attack, waiting for the last explosions, bonus tally, or THE END.
RampWavesOptions
interface RampWavesOptions
Options for rampWaves. Each ramp is [start, perWave, limit].
waves: number— Waves to generate (the last repeats forever).icbms: [number, number, number]— ICBMs per wave.speed: [number, number, number]— Attack speed, units per frame.smartBombs?: [number, number, number]— Smart bombs per wave, from wavesmartFrom(default none).smartFrom?: number— First wave with smart bombs (default 1).launchBelowY: [number, number, number]—launchBelowYper wave.flier?: { from: number; minY: number; maxY: number; cooldown: [number, number, number]; fireDelay: [number, number, number]; burst: number }— Fliers from wavefrom(default none). Cooldown and fire delay ramp downward.
TallyState
interface TallyState
End-of-wave tally progress.
phase: "abms" | "targets" | "pause"— Counting ABMs, then targets, then a pause.battery: number— Battery being counted.target: number— Next target to count.abms: number— ABMs counted.targets: number— Targets counted.points: number— Points awarded by this tally.carry: number[]— Ammunition per battery when the wave ended (for carry-over replenishment).
TargetDef
interface TargetDef
One protected target (an arcade "city"). Losing every target ends the game.
id: string— Stable id.kind?: string— Free-form kind for presentation (default "city").x: number— Centre x.y: number— Impact height: an ICBM aimed at it lands here.width: number— Width (presentation and the bot's danger test).bonus: number— Points (times the multiplier) for surviving a wave. Original: 100.
TargetState
interface TargetState
One target now.
id: string— Target id.kind: string— Kind (presentation).x: number— Centre x.y: number— Impact y.width: number— Width.alive: boolean— Standing.
WaveDef
interface WaveDef
One wave of the attack. The last entry repeats for every later wave.
icbms: number— ICBMs this wave (top launches, MIRV children and flier volleys all draw on it).smartBombs: number— Smart bombs this wave.speed: number— Attack speed in units per frame (ICBMs and smart bombs). Original: 1 / (1 + delay).launchBelowY: number— New attacks wait until the highest attack in flight is below this y. Original: altitude 202 - 2 x wave, at least 180.flier: FlierWave | null— Flier rules, or null for no fliers. Original: none on wave 1.


