The pack
excavation
function excavation(): Pack<{ excavation: ExcavationApi }>
Install the renderer-independent excavation API: diggable terrain, tunnelling bodies, falling objects.
Helpers
createExcavation
function createExcavation(config: ExcavationConfig): Excavation
Build a diggable-terrain field. Throws a readable error for an unsupported configuration.
EXCAVATION_LEGEND
const EXCAVATION_LEGEND: Readonly<Record<string, Terrain>>
The default map legend.
Classes
Excavation
class Excavation
A field of diggable terrain. Create one with createExcavation. Fixed-step and deterministic: the same
config, the same inputs and the same step sequence produce the same state, and snapshot/restore
round-trip it exactly.
readonly cols: number— Columns of cells.readonly rows: number— Rows of cells.readonly subdivisions: number— Sub-cells per cell edge.readonly tunnelWidth: number— Default carve width as a fraction of a cell.readonly strata: readonly number[]— Start rows of the strata, ascending.readonly collectRadius: number— Pickup radius in cells.constructor(config: ExcavationConfig)inBounds(x: number, y: number): boolean— Whether a cell lies inside the field.terrainAt(x: number, y: number): Terrain | null— The authored terrain of a cell (what it was built as; whether it is dug now isisOpen/dugFraction).isBedrock(x: number, y: number): boolean— Bedrock or outside the field: nothing can dig it.stratum(row: number): number— Index of the stratum (layer of earth) a row belongs to; -1 above the first stratum.subcellDug(gx: number, gy: number): boolean— Whether sub-cell (gx, gy) is dug. Sub-cells are numbered from the field's top-left,subdivisionsper cell.isDugAt(x: number, y: number): boolean— Whether the point (x, y) in cell units lies in dug space. Outside the field is never dug.dugFraction(x: number, y: number): number— Fraction of a cell's sub-cells that are dug (0..1).isOpen(x: number, y: number): boolean— Whether a cell's centre is dug through: a body can stand in it and an object can fall into it.connected(a: Cell, b: Cell): boolean— Whether two orthogonally adjacent cells are joined by dug earth along the line between their centres.openings(x: number, y: number): Direction[]— Directions with a dug connection out of an open cell (ignores objects and bodies).region(x: number, y: number): number— Connectivity region of an open cell: two cells share a region when a body could walk between them along dug connections. -1 for a cell that is not open. Recomputed lazily after digging.distances(target: Cell): Int32Array— Tunnel distance in cells from every cell totargetalong dug connections (-1 where unreachable or not open). Cached until the terrain changes.tunnelDistance(from: Cell, to: Cell): number— Tunnel distance between two cells, or -1 when one cannot walk fromfromtoto.route(from: Cell, to: Cell): Direction | null— The first step along the shortest dug route fromfromtoto, or null when there is none.nearestOpen(p: Cell, maxRadius = Math.max(this.cols, this.rows)): Cell | null— The nearest open cell to a point (by grid distance, ties in scan order), or null when nothing is dug.carve(x0: number, y0: number, x1: number, y1: number): number— Dig every sub-cell whose centre lies in the rectangle [x0, x1] x [y0, y1] (cell units), except in bedrock. Returns how many sub-cells were newly dug.digCell(x: number, y: number): number— Dig a whole cell (every sub-cell). Returns how many sub-cells were newly dug.digTunnel(from: Cell, to: Cell, width = this.tunnelWidth): number— Dig a straight tunnel between two cell centres with a given width (defaulttunnelWidth).fill(x0: number, y0: number, x1: number, y1: number): number— Refill the rectangle with earth (a hole closing up). Bedrock and sky cells are left alone. Returns sub-cells filled.takeChanges(): Cell[]— Cells whose dug sub-cells changed since the last call (for a renderer redrawing only what changed).addBody(spec: BodySpec): BodyState— Add a body. Digging bodies dig out the cell they start in.removeBody(id: string): void— Remove a body (it died, escaped or left).body(id: string): BodyState | undefined— A copy of one body's state, or undefined.get bodies(): BodyState[]— Copies of every body, in update order.steer(id: string, direction: Direction | null): void— Steer a held body: a direction moves it (cornering when the turn opens), null stops it.want(id: string, direction: Direction): void— Ask an auto body to take a direction at its next opportunity (overrides one decision).setSpeed(id: string, speed: number): void— Change speed (cells per second).setFrozen(id: string, frozen: boolean): void— Freeze or release a body (a frozen body does not move).setBlocking(id: string, blocking: boolean): void— Make a body block (or stop blocking) other grid bodies.setFlags(id: string, flags: { collects?: boolean; pushes?: boolean; crushable?: boolean; control?: "held" | "auto"; digFactor?: number }): void— Set the remaining behaviour flags of a body.setMode(id: string, mode: BodyState["mode"], ghost?: { target: Cell; policy?: GhostPolicy }): void— Change how a body moves. Enteringghostneeds a target (updated later withghostTarget); it lands by itself underpolicyand becomestunnelagain. Leaving ghost mode directly snaps it to its nearest cell.ghostTarget(id: string, target: Cell): void— Move a ghost's target (a ghost chasing a moving player).place(id: string, at: Cell, direction?: Direction): void— Put a body on a cell centre (a respawn): clears ghost, caught, crushed, frozen and steering.exits(id: string): Direction[]— Directions a body could move from its nearest cell right now: dug connections for tunnel bodies, any diggable neighbour for diggers, never into bedrock, objects it cannot push or blocking bodies.addObject(spec: { id: string; kind: string; at: Cell }, ids = new Set(this.objectList.map((o) => o.id))): ObjectState— Add a loose object at runtime (an apple a monster turned into).removeObject(id: string): void— Remove a loose object (eaten by a monster).get objects(): ObjectState[]— Copies of every loose object (gone ones are removed).addItem(spec: { id: string; kind: string; at: Cell; group?: string }, ids = new Set(this.itemList.map((i) => i.id))): ItemState— Add a collectible at runtime (a bonus vegetable).removeItem(id: string): void— Remove a collectible without collecting it (a bonus timing out, a monster eating it).get items(): ItemState[]— Copies of the collectibles still in the field.objectIn(x: number, y: number): ObjectState | undefined— The object resting (or wobbling, or crumbling) in a cell; falling objects are not in a cell.step(dt: number, options: StepOptions = {}): void— Advance the field bydtseconds: bodies (in order), pushes, pickups, then loose objects.get time(): number— Simulated seconds.drain(): ExcavationEvent[]— Take the events since the last drain.snapshot(): ExcavationSnapshot— A complete serializable copy of the field's mutable state.restore(snap: ExcavationSnapshot): void— Restore a snapshot taken from a field built from the same config.
Types
BodyControl
type BodyControl = "held" | "auto"
Who drives a grid body. held: it moves only while steered (a player's held stick) and corners when the
wanted turn opens. auto: it always moves at its speed and chooses a direction at every cell centre (the
decide hook of step, or a seeded wander).
BodyMode
type BodyMode = "dig" | "tunnel" | "ghost" | "fixed"
How a body moves. dig: on the grid, carving earth as it goes (only bedrock, objects and blocking bodies stop
it). tunnel: on the grid, only along dug connections between cells. ghost: freely through anything,
towards a target, until it lands in an open cell (then it becomes tunnel again). fixed: does not move.
BodySpec
interface BodySpec
A body to add to the field.
id: string— Unique id.kind?: string— Free-form kind for the game ("player", "pooka" ...). Default "body".at: Cell— Start cell (its centre).direction?: Direction— Initial heading. Default "left".mode?: BodyMode— Movement mode. Default "tunnel".control?: BodyControl— Steering. Default "auto".speed: number— Speed in cells per second.digFactor?: number— Speed multiplier while carving fresh earth (dig mode). Default 1.carveWidth?: number— Width of the tunnel it carves, as a fraction of a cell. Default: the field'stunnelWidth.turnBefore?: number— Cornering window before a cell centre, in cells (0 <= w < 0.5): a perpendicular turn wanted this far short of a turn line is taken at once, sliding onto the line. Default 0 (turns only exactly on a centre, as the arcade enemies do). A player usually wants about 0.4 so a turn pressed a little early is never deferred.turnAfter?: number— Cornering window after a cell centre, in cells (0 <= w < 0.5): a turn wanted just past a line is taken there, sliding back onto it, rather than a whole cell later. Default 0.collects?: boolean— Picks up collectibles it passes. Default false.pushes?: boolean— Pushes pushable objects along tunnels when walking into them sideways. Default false.crushable?: boolean— Can be caught and crushed by falling objects. Default true.blocking?: boolean— Blocks other grid bodies from entering its cell. Default false.
BodyState
interface BodyState
Serializable state of one body.
id: string— Unique id.kind: string— Free-form kind.mode: BodyMode— Movement mode.control: BodyControl— Steering.x: number— Position in cell units (centre).y: number— Position in cell units (centre).direction: Direction— Heading (the last grid direction it moved or wants).moving: boolean— Whether it moved during the last step.speed: number— Speed in cells per second.digFactor: number— Speed multiplier while carving fresh earth.carveWidth: number— Carve width as a fraction of a cell.turnBefore: number— Cornering window before a centre, in cells.turnAfter: number— Cornering window after a centre, in cells.steer: Direction | null— The held direction (held control) or null.frozen: boolean— Frozen bodies do not move (stunned, inflated, caught).blocking: boolean— Blocks other grid bodies.collects: boolean— Collects items.pushes: boolean— Pushes objects.crushable: boolean— Can be crushed.digging: boolean— Whether it carved fresh earth during the last step.caughtBy: string | null— Id of the falling object carrying it, or null.crushed: boolean— Set once an object has landed on it.ghost: GhostState | null— Ghost state while in ghost mode.walker: GridWalkerState— The grid walker's state (grid modes).
Cell
interface Cell
A cell, or a point in cell units.
x: number— Column (or x in cell units).y: number— Row (or y in cell units), 0 at the top.
DecideContext
interface DecideContext
Context passed to a decide hook at a cell centre.
body: BodyState— The body deciding (a copy).at: Cell— The cell it is centred on.exits: Direction[]— Directions it can move from here (for tunnel bodies: dug connections; for diggers: any non-blocked cell).
Direction
type Direction = "up" | "left" | "down" | "right"
One of the four grid directions.
ExcavationApi
type ExcavationApi = typeof api
game.excavation: the diggable-terrain field (createExcavation), its legend and types.
ExcavationConfig
interface ExcavationConfig
Everything a field is built from.
map: string[]— The terrain, one string per row, all the same length. Default legend:#earth, space or.tunnel,~sky,=bedrock. Any number of columns and rows (up to the documented cell budget).legend?: Record<string, Terrain>— Extra or replacement map characters.subdivisions?: number— Sub-cells per cell edge (2..16). Default 4. Higher is a finer dig.tunnelWidth?: number— Width a digging body carves, as a fraction of a cell (0 < w <= 1). Below 1, parallel tunnels keep a wall between them. Default 0.75.strata?: number[]— Start row of each stratum (layer of earth), ascending. Default: one stratum from row 0.tunnels?: TunnelSpec[]— Straight tunnels carved at start (as a body would dig them).objectKinds?: Record<string, LooseObjectKind>— Loose object kinds.objects?: ObjectSpec[]— Loose objects.items?: ItemSpec[]— Collectibles.collectRadius?: number— Radius in cells within which a collecting body picks an item up. Default 0.45.seed?: number— Seed for the default wander policy. Default 1.
ExcavationEvent
type ExcavationEvent = | { kind: "dig"; id: string; cell: Cell; subcells: number; distance: number } | { kind: "collect"; item: string; itemKind: string; group: string; by: string; at: Cell } | { kind: "loosen"; object: string; at: Cell } | { kind: "fall"; object: string; at: Cell } | { kind: "catch"; object: string; body: string } | { kind: "land"; object: string; at: Cell; fell: number; broken: boolean; caught: string[] } | { kind: "remove"; object: string } | { kind: "push"; object: string; by: string } | { kind: "ghost"; id: string } | { kind: "materialise"; id: string; at: Cell }
Something that happened during a step. Drain them with drain().
ExcavationSnapshot
interface ExcavationSnapshot
A complete serializable snapshot of a field.
dug: string— Dug sub-cells, packed bits as hex.bodies: BodyState[]— Bodies in update order.objects: ObjectState[]— Loose objects.items: ItemState[]— Collectibles still in the field.random: RngState— The wander stream: the kernel generator's state (four words, fromrng.snapshot()), so a restore repeats the draws.time: number— Simulated seconds.
GhostPolicy
interface GhostPolicy
Where a ghost may land.
minSeconds?: number— Minimum seconds as a ghost before it may land. Default 0.5.maxSeconds?: number— After this many seconds any open cell will do. Default: never relax.landing?: "any" | "other-region" | "target-region"—any: any open cell.other-region: an open cell not connected to the tunnel it left.target-region: an open cell connected to the target's cell. Defaultother-region.row?: number— Only land on this row (an escape to the surface).keepAway?: number— Never land within this many cells of the target (eyes do not reappear on top of the player). Default 0.
GhostState
interface GhostState
Ghost state of a body in ghost mode.
target: Cell— The point it floats towards.elapsed: number— Seconds spent as a ghost.policy: GhostPolicy— The landing policy.origin: number— Connectivity region of the tunnel it left (-1 when it left from earth).landing: Cell | null— The open cell it is settling into, once chosen.
ItemSpec
interface ItemSpec
A collectible embedded in the earth (or lying in a tunnel).
id: string— Unique id.kind: string— Free-form kind ("cherry", "gold", "vegetable").at: Cell— Cell it sits in.group?: string— Optional group id (a Mr. Do! cherry cluster).
ItemState
interface ItemState
Serializable state of one collectible.
id: string— Unique id.kind: string— Free-form kind.x: number— Cell it sits in.y: number— Cell it sits in.group: string— Group id or "".
LooseObjectKind
interface LooseObjectKind
Behaviour of one kind of loose object (a rock, an apple, a boulder).
wobbleSeconds?: number— Seconds it wobbles after losing support before it falls. Default 0.5.fallSpeed?: number— Fall speed in cells per second. Default 6.heldBy?: string[]— Body kinds that hold it up: while one of them stands directly beneath it (the digger who undermined it), it does not start to wobble. Default none.breakAfter?: number— Cells fallen at which landing breaks it. 0 always breaks (a Dig Dug rock crumbles wherever it lands); 2 breaks after falling more than its own height (a Mr. Do! apple); a negative value never breaks. Default 0.crumbleSeconds?: number— Seconds a broken object stays (crumbling) before it is removed. Default 0.6.pushable?: boolean— Bodies that push can shove it sideways along tunnels. Default false.catchWidth?: number— Half-width of the catch zone in cells: bodies whose centre is closer than this horizontally are caught. Default 0.6.
ObjectPhase
type ObjectPhase = "rest" | "loose" | "falling" | "landed" | "gone"
Lifecycle of a loose object.
ObjectSpec
interface ObjectSpec
A loose object to place.
id: string— Unique id.kind: string— Kind key intoobjectKinds.at: Cell— Cell it rests in.
ObjectState
interface ObjectState
Serializable state of one loose object.
id: string— Unique id.kind: string— Kind key.x: number— Centre in cell units.y: number— Centre in cell units.phase: ObjectPhase— Lifecycle.timer: number— Seconds left in the current phase (wobble or crumble).fromY: number— The y where the current fall began.fell: number— Cells fallen in the last fall.caught: string[]— Bodies caught by the current fall.broken: boolean— Whether it broke on landing.pushedBy: string | null— Id of the body pushing it during the last step, or null.
StepOptions
interface StepOptions
Options for one step.
decide?: (ctx: DecideContext) => Direction | null— Chooses the heading of anautobody at each cell centre. Return a direction fromexits, or null to let the field's seeded wander choose. Called in body order; deterministic if the hook is.
Terrain
type Terrain = "earth" | "tunnel" | "sky" | "bedrock"
What a map character means. earth is diggable and starts solid. tunnel starts dug the way a body digs
(walls left between parallel tunnels) and joins every orthogonal tunnel/sky neighbour. sky starts
completely clear (a surface row). bedrock is solid and can never be dug.
TunnelSpec
interface TunnelSpec
A pre-dug straight tunnel between two cells in the same row or column.
from: Cell— One end.to: Cell— The other end.


