The pack
digTrap
function digTrap(): Pack<{ digTrap: typeof api }>
Install the renderer-independent dig and trap API.
Helpers
classicLevels
function classicLevels(): ClassicLevel[]
The bundled classic levels with their original numbers.
createDigTrapGame
function createDigTrapGame(config: DigTrapGameConfig): DigTrapGame
Create a dig-and-trap game from a definition (a preset or your own).
createHoles
function createHoles(o: HolesOptions): Holes
Create the hole mechanic over a walker world.
digTrapDifficulty
function digTrapDifficulty(name: "easy" | "normal" | "arcade"): DigTrapDifficulty
Named difficulty presets.
LODE_RUNNER_TICKS
const LODE_RUNNER_TICKS: { readonly across: 5; readonly vertical: number; readonly dig: 11; readonly digLimit: 8; readonly open: 166; readonly closing: readonly [8, 8, 4]; readonly shake: 66; readonly reborn: 8; readonly guardMoves: readonly [0, 3, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]; }
Lode Runner timing, in game ticks, from Lode Runner Total Recall (AI version 4, which its author tuned to match the Apple II): the runner moves 8/40 of a tile across and 9/44 of a tile up or down per tick; a dig takes 11 ticks and a guard over the site cancels it during the first 8; a hole stays open 166 ticks and refills over 8 + 8 + 4; a trapped guard shakes for 51 + 5 x 3 ticks before climbing out; a reborn guard appears after 8 ticks. Guards share a fixed number of moves per 6 ticks: 3 for one guard, then 6, 8, 10, 12 ... (2 more for each guard).
lodeRunnerGuard
function lodeRunnerGuard(tile = 8, tickRate = 23): DigTrapKind
The Lode Runner guard kind: the runner's moves at the shared guard pace, the 1983 pursuit decision.
lodeRunnerLegend
function lodeRunnerLegend(): Record<string, LegendEntry>
The classic Lode Runner legend: ' ' '#' '@' 'H' '-' 'X' 'S' '$' '0' '&'.
lodeRunnerLevels
function lodeRunnerLevels(): DigTrapLevel[]
The bundled Apple II levels (1, 2, 3, 4, 5, 6, 10 and 20) as dig-trap levels.
lodeRunnerPreset
function lodeRunnerPreset(o: LodeRunnerOptions = {}): DigTrapGameConfig
Lode Runner (Broderbund, 1983): dig left or right into the brick beside and below (not under a ladder, bar, chest or guard), holes refill and crush, guards fall in and drop their gold, climb out, die in refilled holes and are reborn on the second row, runners walk over guards' heads, gold 250, guard trapped 75, killed 75, level 1500, five lives plus one per level, the hidden ladder appears when every chest is taken and the level ends at the top row.
lodeRunnerProfiles
function lodeRunnerProfiles(tile = 8, tickRate = 23): { runner: WalkerProfile; guard: WalkerProfile }
The runner and guard profiles at a tick rate: grid-aligned moves of one tile per 5 ticks across, 4.9 up and down.
parseLodeRunnerLevel
function parseLodeRunnerLevel(text: string | readonly string[], o: { width?: number; number?: number } = {}): DigTrapLevel
Import a level in the classic Lode Runner text format: one 448-character string (28 x 16, row by row, as Total Recall stores it), a newline-separated block, or an array of rows. '.' is read as empty space. Rows of any equal width and any count are accepted (custom sizes); short rows are padded with spaces. Throws on characters outside the format or a missing runner (a second runner is ignored, as in the original).
spacePanicKinds
function spacePanicKinds(tile = 8): Record<string, DigTrapKind>
The alien kinds: Monster (red, one floor), Boss (green, two), Don (white, three); chasing over floors and ladders with wandering spells.
spacePanicLegend
function spacePanicLegend(): Record<string, LegendEntry>
The Space Panic legend: '%' diggable girder (one-way floor), 'h' ladder through a girder, 'H' ladder, '#' ground, 'P' player, 'E' alien entry.
spacePanicLevels
function spacePanicLevels(screen: string[] = spacePanicScreen()): DigTrapLevel[]
The level table: aliens released per level and their speed (later levels add Bosses and Dons and run faster).
spacePanicPreset
function spacePanicPreset(o: SpacePanicOptions = {}): DigTrapGameConfig
Space Panic (Universal, 1980): girder floors joined by ladders, no jump; dig a hole with several strokes, aliens that fall in are held head-down in the girder, hit them through it (three hits) and they fall; a Monster dies after one floor, a Boss after two, a Don after three, scoring more for longer drops (Monster 100-800, Boss 300-1,200, Don 800-2,000 as the operator's manual lists), and one alien landing on another kills both. Left too long, an alien climbs out, fills its hole and turns into the next kind. Falling into a hole or touching an alien costs a life, and so does running out of oxygen; the oxygen left at the end of a level scores 10 a second. Three lives, an extra life at 3,000.
spacePanicScreen
function spacePanicScreen(): string[]
The Space Panic screen: 24 x 30 cells of 8 px (the 192 x 256 portrait screen less the score line), five girder floors over the ground joined by staggered ladders, aliens entering at the top. Redrawn from screenshots.
validateDigTrapGame
function validateDigTrapGame(c: DigTrapGameConfig): void
Check a definition; throws a clear error for anything unsupported.
validateHoleRules
function validateHoleRules(r: HoleRules): void
Validate hole rules; throws a clear error for an unsupported configuration.
Types
ClassicLevel
interface ClassicLevel
A classic level: its number in the 150-level set and its rows.
number: number— Level number in the Apple II set.rows: string[]— 16 rows of 28 characters.
DifficultyInput
type DifficultyInput = "easy" | "normal" | "arcade" | DigTrapDifficulty
Difficulty by name or explicit values.
DigTrapDifficulty
interface DigTrapDifficulty
The difficulty knobs every preset scales. "arcade" values are all 1 (the original tables).
speed: number— Enemy speed multiplier.hold: number— Multiplier on how long prisoners stay in holes before climbing out.open: number— Multiplier on how long holes stay open before closing.oxygen: number— Multiplier on the oxygen supply.maxEnemies: number— Most enemies on screen at once (applied after the level's own count); Infinity for no cap.promote: boolean— Escaped prisoners are promoted to their kind'spromote.
DigTrapEnemyPhase
type DigTrapEnemyPhase = "active" | "trapped" | "climbing" | "knocked" | "reborn"
What an enemy is doing, for renderers.
DigTrapEnemyView
interface DigTrapEnemyView
An enemy as the rules see it.
id: string— Body id (stable for its life; a promotion makes a new one).kind: string— Kind.phase: DigTrapEnemyPhase— Phase.carrying: boolean— Carrying an item.timer: number— Seconds left in the hole before climbing out (trapped; -1 when prisoners never climb out), or before reappearing (reborn).
DigTrapEvent
type DigTrapEvent = | { kind: "level-start"; level: number; number: number } | { kind: "item"; by: string; col: number; row: number; points: number } | { kind: "item-dropped"; by: string; col: number; row: number } | { kind: "item-lost"; by: string } | { kind: "revealed"; cells: number } | { kind: "dug"; col: number; row: number } | { kind: "stroke"; col: number; row: number; strokes: number; of: number } | { kind: "filled"; col: number; row: number; cause: string } | { kind: "trapped"; id: string; col: number; row: number; points: number } | { kind: "escaped"; id: string; promotedTo: string } | { kind: "hit"; id: string; hits: number; of: number } | { kind: "knocked"; id: string; col: number; row: number } | { kind: "enemy-killed"; id: string; enemy: string; cause: "crushed" | "knocked" | "together"; floors: number; points: number; x: number; y: number } | { kind: "enemy-survived"; id: string; floors: number; needed: number } | { kind: "enemy-released"; id: string; enemy: string } | { kind: "respawned"; id: string; col: number; row: number } | { kind: "player-died"; cause: "enemy" | "crushed" | "fell-in" | "oxygen" | "suicide" | "fall" | "out-of-bounds"; x: number; y: number } | { kind: "level-complete"; level: number; points: number; bonus: number } | { kind: "extra-life"; lives: number } | { kind: "game-over"; score: number }
Events from drain().
DigTrapGame
interface DigTrapGame
A running dig-and-trap game.
readonly config: DigTrapGameConfig— The definition (difficulty applied).readonly map: PlatformMap— The current level's map.readonly world: WalkerWorld— The current walker world.readonly enemies: EnemyDirector— The current enemy director.readonly holes: Holes— The current hole mechanic.readonly player: string— Player body id ('player').readonly state: DigTrapState— A copy of the state.readonly build: number— Increments whenever the level is rebuilt (renderers snap every sprite).step(input: DigTrapInput): void— One fixed step.tick(seconds: number, input: DigTrapInput): number— Run whole fixed steps for elapsed seconds (the remainder carries to the next call; at most 8 steps of backlog); returns the number run. A dig or fill press is held until the next step runs and applies to that step only.newGame(level?: number): void— Start a new game (optionally at a level index).suicide(): void— Lose the life in play (Lode Runner's abort key).kindOf(id: string): string— Kind of an enemy body id, or ''.drain(): DigTrapEvent[]— Events since the last drain.snapshot(): DigTrapSnapshot— Serialisable state.restore(s: DigTrapSnapshot): void— Restore a snapshot taken from this game.
DigTrapGameConfig
interface DigTrapGameConfig
A complete dig-and-trap game definition.
name: string— Free name ('lode-runner', 'space-panic', ...).tile: number— Tile size in world units.legend: Readonly<Record<string, LegendEntry>>— Map legend. Markers:player,enemy,item. Tags the rules read:reveal(hidden until every item is taken).bounds?: PlatformMapDef["bounds"]— Map bounds (default sides and bottom solid, top open).levels: DigTrapLevel[]— The levels, played in order.loopTo?: number— Level index play continues from after the last level. Default 0.player: WalkerProfile— The player's movement profile.digMode: "sides" | "facing"— How the player digs: 'sides' (two dig buttons, left and right: Lode Runner) or 'facing' (one button digs ahead: Space Panic).kinds: Record<string, DigTrapKind>— Enemy kinds by name.markerKind: string— Kind placed atenemymarkers when a level lists noenemies.maxEnemies: number— Most enemies a level starts with (extra markers are ignored, as Lode Runner caps its guards).pace?: number[]— Enemy pace by enemy count: entry n is the speed multiplier with n enemies on the level (the last repeats). Lode Runner shares a fixed number of guard moves among its guards, so each slows as more are added. Default [1].release?: { initial: number; every: number }— Release schedule: enemies at the start, then one everyeveryseconds at the enemy markers in turn. Default: all at once.holes: HoleRules— Hole rules (blockers are added by the game: items block a dig beneath them).objective: "collect" | "clear"— The objective: 'collect' every item then reachexitRow(Lode Runner), or 'clear' every enemy (Space Panic).exitRow?: number— Row the player must reach once every item is taken ('collect'). Default 0.enemyCarry: [number, number] | null— Enemies pick up items they pass and carry them for a random number of cells in [min, max] before dropping them.null: they ignore items.respawn: { row: number; delay: number } | null— Dead enemies respawn: in a random free cell ofrow(scanning down when a row is full) afterdelayseconds;null: they stay dead.contact: { x: number; y: number; trappedHarmless: boolean }— Contact kills the player when centres are within these fractions of a tile on both axes.trappedHarmless: prisoners never kill.scoring: { /** Item taken. */ item: number; /** Enemy trapped in a hole. */ trapped: number; /** Enemy crushed by a closing hole. */ crushed: number; /** Level completed. */ level: number; /** Points per second of oxygen left at a level's end. */ oxygenBonus: number; }— Points.lives: number— Starting lives.lifePerLevel: number— Lives gained per completed level.extraLives: number[]— Scores that each award one extra life.oxygen: number | null— Oxygen seconds per level (null: no timer). Running out costs a life.step?: number— Fixed step, seconds. Default 1/60.timing: { ready: number; dying: number; complete: number }— Phase lengths, seconds: before play, after a death, after a level.difficulty: DigTrapDifficulty— Difficulty.startLevel?: number— Level to start on (index). Default 0.seed: number— Seed.
DigTrapInput
interface DigTrapInput
What the player does this step.
x: number— -1 left .. 1 right.y: number— -1 up .. 1 down.dig?: number | boolean— Dig: -1 left, 1 right ('sides'), or true / any non-zero for ahead ('facing').fill?: number | boolean— Fill (Space Panic): -1 / 1 / true likedig.
DigTrapKind
interface DigTrapKind
An enemy kind in a dig-and-trap game: a platform-enemies kind plus how holes and falls treat it.
enemy: EnemyKind— The enemy (profile and movement) for platform-enemies.floors?: number— Floors a knocked body must fall through in one drop to die (Space Panic: Monster 1, Boss 2, Don 3). Default 1.knockScore?: number[]— Points for a knock kill by floors fallen: entryfloors - 1, the last entry repeating. Default [0].promote?: string— Kind it turns into after climbing out of a hole ('' or absent: none).
DigTrapLevel
interface DigTrapLevel
One authored level.
rows: string[]— Rows through the game's legend (top first).number?: number— Number shown for the level (default index + 1).enemies?: { kind: string; count: number }[]— Enemies released over time, by kind (default: onemarkerKindat each enemy marker).speed?: number— Enemy speed multiplier for this level (default 1).oxygen?: number— Oxygen seconds for this level (default the game's).hold?: number— Multiplier on how long prisoners stay in holes (default 1).
DigTrapSnapshot
interface DigTrapSnapshot
Serialisable game snapshot.
state: DigTrapState— Rules state.map: PlatformMapSnapshot— Map.world: WalkerWorldSnapshot— Walker world.enemies: EnemiesSnapshot— Enemies.holes: HolesSnapshot— Holes.extra: { carry: [string, number][]; last: [string, string][]; reborn: [string, number][]; release: number; rng: RngState; marker: number; events: DigTrapEvent[]; items: string[]; acc: number }— Private: carry counters, last cells, reborn timers, release timer, rng.
DigTrapState
interface DigTrapState
Serialisable state, for renderers and snapshots.
status: DigTrapStatus— Status.timer: number— Seconds in the status.level: number— Level index.levelNumber: number— Level number shown.score: number— Score.lives: number— Lives left (including the one in play).items: { col: number; row: number }[]— Items lying on the map.itemsLeft: number— Items not yet taken (lying or carried).exitOpen: boolean— Hidden cells revealed and the exit open ('collect').oxygen: number | null— Oxygen seconds left (nullwithout a timer).oxygenMax: number | null— Oxygen at the level's start.enemies: DigTrapEnemyView[]— Enemies on the level.pending: string[]— Enemies still to be released.killed: number— Enemies killed this level.awarded: number— Extra-life thresholds already awarded.cause: string— Why the last life was lost ('' none).
DigTrapStatus
type DigTrapStatus = "ready" | "playing" | "dying" | "complete" | "over"
Game status.
Hole
interface Hole
One hole (a dug or partly dug cell).
col: number— Column.row: number— Row of the dug cell.state: HoleState— Stage.strokes: number— Dig strokes completed (equalsrules.strokesonce open).fill: number— Fill strokes applied to an empty open hole.hits: number— Hits applied to the prisoner.timer: number— Seconds in the current stage.stage: number— Closing stage index (state 'closing').occupant: string— Body id held in the hole, or ''.digger: string— Body id digging it now, or ''.
HoleEvent
type HoleEvent = | { kind: "stroke"; digger: string; col: number; row: number; strokes: number; of: number } | { kind: "dug"; digger: string; col: number; row: number } | { kind: "dig-cancelled"; digger: string; col: number; row: number; by: string } | { kind: "filled"; by: string; col: number; row: number; cause: "hand" | "closed" | "escape" | "knock" } | { kind: "closing"; col: number; row: number; stage: number } | { kind: "trapped"; id: string; col: number; row: number } | { kind: "climbing"; id: string; col: number; row: number } | { kind: "escaped"; id: string; col: number; row: number } | { kind: "crushed"; id: string; col: number; row: number } | { kind: "fell-in"; id: string; col: number; row: number } | { kind: "hit"; by: string; id: string; col: number; row: number; hits: number; of: number } | { kind: "knocked"; by: string; id: string; col: number; row: number } | { kind: "landed"; id: string; floors: number; col: number; fromRow: number; row: number; on: string }
Events from drain().
HoleRules
interface HoleRules
How holes are dug, how long they last, what they hold and what closing them does.
strokes: number— Dig strokes needed to open a hole (Lode Runner 1, Space Panic several).strokeTime: number— Seconds one stroke keeps the digger busy (intents ignored).interruptBefore: number— Fraction of a stroke (0..1) before which a body arriving over the dig site cancels the stroke (Lode Runner 8/11). 0: never.open: number | null— Seconds a fully open hole stays before it starts closing;null: it stays until filled by hand or by an escaping prisoner.closing: number[]— Closing stages afteropen, seconds each; the hole refills at the end of the last.fillStrokes: number— Strokes needed to fill an empty hole by hand (each also removes a stroke from a partly dug hole). 0: holes cannot be filled by hand.knockStrokes: number— Strokes (dig or fill, seeknockWith) on an occupied hole that knock the prisoner through the floor. 0: never.knockWith: "dig" | "fill" | "either"— Which strokes count as hits on an occupied hole.hold: number | null— Seconds a trapped body stays before climbing out.null: it never climbs out.climbSpeed: number— Climb-out speed, world units per second (up, then one cell sideways).escapeFills: boolean— The hole refills as soon as an escaping prisoner has left it (Space Panic).sink: "bottom" | "head"— Where a prisoner is pinned: 'bottom' (feet on the hole's floor, Lode Runner) or 'head' (head level with the floor's top, so others walk over it, Space Panic).trapTags: string[]— Bodies with any of these tags are trapped (held) by holes.deadlyTags: string[]— Bodies with any of these tags are reportedfell-inwhen they drop into an open hole (Space Panic's spaceman).heads: "none" | "trapped" | "all"— Which trappable bodies others may stand on: none, trapped prisoners only, or all of them (Lode Runner: runners walk on guards' heads).blockers?: (col: number, row: number) => boolean— Cells above a dig site must be free of these (in addition to solid cells, ladders and bars).
Holes
interface Holes extends WalkerController, SurfaceProvider
The hole mechanic: a walker controller and surface provider.
readonly id: string— Controller / provider id.readonly rules: HoleRules— The rules (live; edit withsetRules).holes(): Hole[]— Copies of the holes.hole(col: number, row: number): Hole | null— The hole at a cell, or null.prisoners(): Prisoner[]— Copies of the prisoners.prisoner(id: string): Prisoner | null— A body's prisoner record, or null.busy(id: string): number— Whether a body is busy with a stroke; seconds left.canDig(id: string, side: -1 | 1): { col: number; row: number; ok: boolean; reason: string }— Which cell a dig from this body towardsidewould hit, and whether it may: 'ok' or a reason.dig(id: string, side: -1 | 1): boolean— One dig stroke beside-and-below towardside(or a hit on an occupied hole). Returns whether a stroke started.fill(id: string, side: -1 | 1): boolean— One fill stroke towardside(fills an empty hole, un-digs a partial one, or hits a prisoner).open(col: number, row: number, by?: string): boolean— Open a hole at once (scripted levels, tests).close(col: number, row: number, cause?: "hand" | "closed" | "escape" | "knock"): boolean— Close a hole at once (crushing what is inside).release(id: string): void— Release a body from any hold (for a game that removes or respawns it).setTargets(ids: string[]): void— Set the bodies prisoners climb out toward.setRules(patch: Partial<HoleRules>): void— Patch the rules (difficulty changes).drain(): HoleEvent[]— Events since the last drain.snapshot(): HolesSnapshot— Serialisable state.restore(s: HolesSnapshot): void— Restore (restore the map and world first).
HolesOptions
interface HolesOptions
Options for createHoles.
world: WalkerWorld— The walker world (its map is dug).rules: HoleRules— Rules.enemies?: EnemyDirector— Enemy director: trapped enemies are suspended and resumed. Optional.targets?: string[]— Ids of bodies prisoners climb out toward (the player).attach?: boolean— Register as a walker controller and surface provider. Default true.id?: string— Controller and provider id. Default 'dig-trap'.
HolesSnapshot
interface HolesSnapshot
Serialisable state.
holes: Hole[]— Holes.prisoners: Prisoner[]— Prisoners.busy: { id: string; left: number; total: number; col: number; row: number; kind: "dig" | "fill" }[]— Busy diggers: id, seconds left, total, hole key, kind.pinned: [string, boolean][]— Pinned bodies' original flight setting.events: HoleEvent[]— Undrained events.targets: string[]— Targets.
HoleState
type HoleState = "digging" | "open" | "closing"
A hole's life stage.
LodeRunnerOptions
interface LodeRunnerOptions
Options for lodeRunnerPreset.
difficulty?: DifficultyInput— Difficulty (default 'normal'); 'arcade' is the original timing.levels?: DigTrapLevel[]— Levels (default the bundled Apple II levels). UseparseLodeRunnerLevelto import more.startLevel?: number— Level index to start on.lives?: number— Starting lives (default 5).maxGuards?: number— Most guards on a level (default 5; Total Recall's cap, extra guard markers are ignored).tickRate?: number— Game ticks per second (default 23, Total Recall's NORMAL speed; 18 SLOW, 29 FAST).tile?: number— Tile size in world units (default 8).holes?: Partial<HoleRules>— Hole overrides (open time, closing, hold ...), in seconds.seed?: number— Seed (guard respawn columns, gold-carry lengths).
Prisoner
interface Prisoner
A trapped, climbing or knocked body.
id: string— Body id.col: number— Hole column.row: number— Hole row.phase: "held" | "up" | "side" | "knocked"— 'held' (in the hole), 'up' / 'side' (climbing out), 'knocked' (falling through the floor).timer: number— Seconds in the phase.dir: -1 | 1— Climb-out direction.
SpacePanicOptions
interface SpacePanicOptions
Options for spacePanicPreset.
difficulty?: DifficultyInput— Difficulty (default 'normal'); 'arcade' is the reference timing.levels?: DigTrapLevel[]— Levels (defaultspacePanicLevels()).startLevel?: number— Level index to start on.lives?: number— Starting lives (default 3; the cabinet's DIP switch offers 3 or 4).extraLife?: number— Extra life score (default 3000; DIP 3000 or 5000).oxygen?: number— Oxygen seconds per level (default 90).tile?: number— Tile size (default 8).holes?: Partial<HoleRules>— Hole overrides.seed?: number— Seed.


