The pack
gridKick
function gridKick(): Pack<{ gridKick: typeof api }>
Install the renderer-independent grid kick API.
Helpers
createKickGame
function createKickGame(config: KickConfig): KickGame
Create a kick-block session.
DIRECTIONS
const DIRECTIONS: readonly Direction[]
The four directions in the arcade's order: up, down, left, right.
generateIceMaze
function generateIceMaze(options: IceMazeOptions): IceMaze
Pengo's ice maze generator (routine $2DA1 with $2E46 and $307C), generalised to any odd board.
The board starts full of ice. Lattice nodes sit at even columns and even rows (on Pengo's 13 × 15 board, 7 × 8 of them); the bottom-left node is opened first. One pass scans the node rows from the bottom up and each row left to right. At an opened node that still has an unopened neighbour two cells away, a path is drawn: a random direction (up, down, left, right) is drawn until it points at an unopened node, the two cells to it are opened, and the path continues from there while up, down or left (and, without the bug, right) still has an unopened node. Ice at odd column and odd row is never opened, which is why Pengo's eggs and diamond blocks are always placed on those cells.
ICE_SLIDE_ROOMS
const ICE_SLIDE_ROOMS: readonly { name: string; map: string[] }[]
The ice-slide rooms. Legend: # steel (never moves), I ice (slides; breaks when kicked against
something), R thick ice (slides; cracks on the first blocked kick, breaks on the second; Sno-Bees cannot
chew it), C crate (moves one cell per kick, never breaks), G gem block (slides, unbreakable), * goal
floor for gems, E ice hiding a Sno-Bee egg, P player, w walrus (patrols), s seal (chases and kicks
blocks at the player). Every room is solvable treating ice as fixed (checked by the pack's tests).
iceSlidePreset
function iceSlidePreset(o: IceSlideOptions = {}): KickConfig
Ice slide: a puzzle-action game through the same API as Pengo. Authored rooms of different sizes; gem blocks must be kicked onto goal tiles (the objective is goals, not enemies); steel, thick ice and one-cell crates; walruses patrol, seals chase and kick blocks at the player (blocks kicked by enemies crush the player), hidden Sno-Bee eggs hatch block-chewing enemies; no wall shake and no diamond line; a different crush table, a slower time-bonus table, an extra life every 20,000, and the game ends after the last room.
kickDifficulty
function kickDifficulty(name: KickDifficultyName): KickDifficulty
Difficulty scaling by name. Arcade is the original; Normal (the gallery default) keeps the arcade's speeds but hides fewer eggs and gives more time before the hurry; Easy also slows enemies and allows one fewer on the board at once.
kickPreset
function kickPreset(name: "pengo", options?: PengoOptions): KickConfig
A named preset: pengo or ice-slide.
The Ice Slide preset: a puzzle-action game on the same rules, built by iceSlidePreset.
Implementation of the overloads above; throws for any name but pengo or ice-slide.
OPPOSITE
const OPPOSITE: Readonly<Record<Direction, Direction>>
The direction opposite each direction.
PENGO_EGGS_EXPORT
const PENGO_EGGS_EXPORT: readonly number[]
Eggs per act in the export sets (per arcade-history): 6, then 8, 9, 10, 11 and 12.
PENGO_EGGS_JAPAN
const PENGO_EGGS_JAPAN: readonly number[]
Eggs per act, Japanese program (routine at $0937): 6 in acts 1–2, 8 in 3–7, 10 in 8–11, 12 in 12–16.
PENGO_MATERIALS
const PENGO_MATERIALS: readonly KickMaterial[]
Pengo's two block kinds: ice (breaks when kicked against something, 30 points, may hide an egg) and diamond.
PENGO_TIME_BONUS
const PENGO_TIME_BONUS: readonly KickTimeBonus[]
Pengo's round-time bonus ($0A2B): under 20 s 5000, 20–29 2000, 30–39 1000, 40–49 500, 50–59 10.
pengoLevel
function pengoLevel(act: number, skill = 1, eggs: "japan" | "export" = "japan"): KickLevel
One act of Pengo's table (1–16). skill is the difficulty DIP switch (0 easy … 3 hardest; the factory
setting is 1). From the disassembly: Sno-Bee move period = 12 − ⌊(act−1)/4⌋ − skill ($324E); they turn
block-eating and one period faster when fewer than ⌊(act−1)/4⌋ + 3 remain ($342E); a minute into a life
they are another period faster ($344B); three hatch at once in acts 1–4 and four from act 5 ($326E).
pengoPreset
function pengoPreset(o: PengoOptions = {}): KickConfig
Pengo (Sega/Coreland, 1982): every rule and number from the disassembly unless a deviation is documented.
pengoSpeed
const pengoSpeed = (period: number): number=> …
Cells per second for an arcade move period (main-loop passes per pixel).
pickOddCells
function pickOddCells( random: KickRandom, count: number, cols: number, rows: number, reject: (c: Cell) => boolean, ): Cell[]
Random cells at odd column and odd row (always solid in a generated maze), as Pengo places eggs and
diamond blocks: x = 2·rand(⌊cols/2⌋) + 1, y = 2·rand(⌊rows/2⌋) + 1, re-drawn while reject says so.
Throws when fewer candidate cells remain than requested.
STEP
const STEP: Readonly<Record<Direction, Cell>>
The unit step of each direction (y grows downward).
Classes
KickBoard
class KickBoard
A sliding-block board. Pure and deterministic: push, breakBlock, hatch and tick(dt) change it, and
tick returns what happened. Positions are in cell units; a moving block's x/y are continuous.
readonly cols: number— Board width in cells (an integer, 1 to 1000).readonly rows: number— Board height in cells (an integer, 1 to 1000).readonly slideSpeed: number— Slide speed in cells per second, 0.1 to 1000. Default 16.25.readonly breakSeconds: number— Seconds a block takes to shatter, 0 to 30; its cell stays blocked meanwhile. Default 0.35.constructor(config: KickBoardConfig)material(id: string)— The resolved material (defaults applied). Throws for an unknown id.get materialIds(): string[]— Every material id.inside(x: number, y: number): boolean— Is (x, y) on the board?add(spec: KickBlockSpec): number— Place a block; throws if the cell is taken. Returns its id.remove(id: number): void— Remove a block at once (no animation, no event).get state(): KickBlockState[]— A copy of every block.block(id: number): Readonly<KickBlockState> | null— The live block record by id (read-only use).blockAt(x: number, y: number): Readonly<KickBlockState> | null— The block resting (or shattering) at (x, y), or null. Sliding blocks are not at rest anywhere.sliding(x: number, y: number): Readonly<KickBlockState> | null— Is a sliding block over (x, y): its current cell or the cell it is entering?free(x: number, y: number): boolean— Is (x, y) on the board with no block at rest, shattering or sliding over it?get slides(): readonly Readonly<KickBlockState>[]— The blocks sliding right now (live records: read them, do not change them).get moving(): boolean— Is a block moving anywhere?get shattering(): boolean— Is a block shattering anywhere?goals(): { x: number; y: number; group: string; covered: boolean }[]— Goal cells and whether a block of their group rests on each.get goalsComplete(): boolean— True when the board has goals and every goal is covered by a resting block of its group.push(x: number, y: number, dir: Direction, by: string): KickPushResult— Kick the block at (x, y) indir. A free cell beyond starts a slide (if the material slides); an obstacle beyond damages a breakable block (crack, then break) and otherwise leaves it where it is.bynames the kicker for the events (the session uses 'player' or an enemy id).breakBlock(x: number, y: number, by: string, rule: "enemy" | "any" = "any"): boolean— Shatter the resting block at (x, y) ifenemyrules allow it (enemyBreakable) or unconditionally.hatch(id: number): boolean— Shatter a block from the inside: its egg hatches (the event carriesby: 'hatch').clearEgg(id: number): void— Remove the egg from a block (it moved, was destroyed or hatched elsewhere).setEgg(id: number, kind: string): void— Put an egg ofkindinto a resting block that may hold one.tick(dt: number, obstacle?: (x: number, y: number, block: Readonly<KickBlockState>) => boolean): KickBoardEvent[]— Advance slides and shattering bydtseconds; returns the events since the last tick.obstacle, when given, is asked at each cell centre whether something the board does not know about (an actor a non-crushing block must not run over) blocks the next cell; a slide stops in front of it.halt(id: number): void— Stop a sliding block where it is (at its current cell centre or, if between cells, back at its cell).line(group: string, length: number): { cells: Cell[]; alongWall: boolean } | null— The first run oflengthresting blocks ofgroupside by side in a row or column, or null.alongWallis true when the run lies on the board's outer row or column parallel to it (Pengo pays half for a diamond line against the wall).snapshot(): KickBoardSnapshot— A plain-data copy forKickBoard.restore.restore(s: KickBoardSnapshot): void— Restore aKickBoard.snapshot.
KickGame
class KickGame
A kick-block arcade session. Feed it control(direction) and kick(), call tick(dt) (fixed 120 Hz steps
inside), read state and drain() events. Deterministic from the seed; snapshot()/restore() round-trip.
readonly config: KickConfig— The configuration (normalised).readonly difficulty: KickDifficulty— Difficulty in force.constructor(config: KickConfig)row(level = this.s.level): KickLevel— The level-table row for a level number (1-based), with looping applied.reset(): void— Start a new game at the configured level.control(direction: Direction | null): void— Hold a direction (null to stop). The player moves cell to cell and turns at cell centres.kick(): void— Kick: push the block the player faces, or shake the border. Remembered briefly until a cell centre.get kickBoard(): KickBoard— The live board (read it; change it only through the session).get state(): KickState— A copy of the state.drain(): KickEvent[]— Events since the last drain.remaining(): number— Live enemies (not finishing) plus eggs.tick(dt: number): KickState— Advance bydtseconds (fixed 120 Hz steps inside; at most 10 s per call). Returns the state.goToLevel(level: number): void— Jump to a level (keeps score and lives).snapshot(): KickSnapshot— A plain-data snapshot.restore(snap: KickSnapshot): void— Restore a snapshot taken from a session with the same configuration.
KickRandom
class KickRandom
The 1982 Pengo board's random generator (routine $2D7C of the Z80 program), reproduced bit for bit: the 16-bit seed is tripled, the old low byte is added into the new high byte, and that high byte is the result. The maze generator, egg and diamond placement and every Sno-Bee decision draw from this one stream, as on the arcade board, so a seed fixes a whole round.
constructor(seed = 1)— A seed of 0 would stay 0 forever (0 × 3 + 0); it is replaced by 1.get state(): number— The current 16-bit seed, for snapshots. Restore a seed taken fromstate.set state(value: number)— The current 16-bit seed, for snapshots. Restore a seed taken fromstate.byte(): number— One byte, exactly as the arcade routine returns it.below(n: number): number— An integer in [0, n). Like the arcade (and $07 / cp $06 / jr nc), it masks to the next power of two and rejects values out of range. The arcade generator has short cycles for some seeds, so after 64 rejections the value is reduced modulo n instead of looping forever.chance(n: number): boolean— True with probability 1 / n (the arcade'sand $07 / jp zis chance(8)).
Constants
PENGO_LOOP_HZ
const PENGO_LOOP_HZ: 780
The arcade's main loop runs about 780 times a second (jotd's 68000 port calibrates 13 passes per 60 Hz frame against MAME), and actors move one pixel each time their period counter fills. A 16-pixel cell at period p therefore takes 16·p/780 s: speed = 48.75 / p cells per second.
Types
Cell
interface Cell
A grid cell: column x, row y, 0 at the top left.
x: number— The column, 0 at the left.y: number— The row, 0 at the top.
Direction
type Direction = GridDirection
One of the four orthogonal directions (the grid-walker direction type).
IceMaze
interface IceMaze
What generateIceMaze returns.
map: string[]— Rows of '#' (ice) and '.' (open), top row first.carveOrder: Cell[]— The open cells in the order they were carved, for a presenter that draws the maze as the arcade does.sealed: number— Lattice nodes (even column, even row) left uncarved; 0 whencomplete.
IceMazeOptions
interface IceMazeOptions
Options for generateIceMaze.
cols: number— Columns; odd, at least 3. Pengo: 13.rows: number— Rows; odd, at least 3. Pengo: 15.seed?: number— The 16-bit seed of the arcade random generator. Default 1.arcadeBug?: boolean— Reproduce the arcade's path-end bug (routine $307C tests "down" twice and never "right", so a path whose only way on is to the right ends early and the maze is left with more solid ice). Default true, as the arcade. False gives the routine its intended right-hand test.complete?: boolean— Keep scanning until every lattice node is carved (a perfect maze with no sealed ice pockets). The arcade makes one bottom-to-top pass and can leave pockets of solid ice. Default false.random?: KickRandom— A generator to draw from instead of a fresh one; it is advanced, as the arcade's shared stream is.
IceSlideOptions
interface IceSlideOptions
Options for the ice-slide preset.
level?: number— Starting room (1-based).lives?: number— Lives. Default 3.seed?: number— Seed.difficulty?: KickDifficultyName | Partial<KickDifficulty>— Difficulty scaling (enemy speed, population). Default normal.rooms?: { name?: string; map: string[] }[]— Rooms to play instead ofICE_SLIDE_ROOMS.enemySpeed?: number— Enemy speed in cells per second. Default 2.6.
KickBlockSpec
interface KickBlockSpec
A block placed by a map or by code.
x: number— The column.y: number— The row.material: string— The material id.egg?: string | null— The enemy kind of a hidden egg.
KickBlockState
interface KickBlockState
A block on the board: where it is, what it is made of, and anything happening to it.
id: number— Stable id for the block's life (a presenter keys its sprite by it).material: string— The material id.cell: Cell— The cell it occupies (its current cell while sliding).x: number— Continuous position in cell units (equal tocellat rest).y: number— Continuous position in cell units.egg: string | null— The enemy kind of the egg inside, or null.damage: number— Kicks against obstacles so far (a cracked block shows damage).motion: { dir: Direction; by: string; travelled: number; from: Cell } | null— Set while sliding.breaking: { remaining: number; by: string; total: number } | null— Set while shattering; the cell stays blocked until it is done.
KickBoardConfig
interface KickBoardConfig
Configures a KickBoard.
cols: number— Columns, 1–1000.rows: number— Rows, 1–1000.materials: KickMaterial[]— The block kinds.blocks?: KickBlockSpec[]— Blocks at the start.goals?: { x: number; y: number; group: string }[]— Goal floor cells, each for a block group.slideSpeed?: number— Slide speed in cells per second. Pengo: 16.25 (one pixel per three of the arcade's main-loop passes).breakSeconds?: number— Seconds a block takes to shatter (the cell stays blocked meanwhile). Pengo: about 0.35.
KickBoardEvent
type KickBoardEvent = | { kind: "slide-start"; block: number; at: Cell; dir: Direction; by: string } | { kind: "slide-step"; block: number; at: Cell; dir: Direction } | { kind: "slide-stop"; block: number; at: Cell; dir: Direction; by: string; travelled: number; group: string | null } | { kind: "break-start"; block: number; at: Cell; by: string; egg: string | null; material: string } | { kind: "broken"; block: number; at: Cell; by: string; egg: string | null; material: string } | { kind: "crack"; block: number; at: Cell; damage: number }
Something that happened on the board during a tick.
KickBoardSnapshot
interface KickBoardSnapshot
A plain-data copy of a board, for snapshots.
blocks: KickBlockState[]— Every block.nextId: number— The next block id.
KickBrain
type KickBrain = "sno-bee" | "patrol" | "chase" | "pusher"
How an enemy kind chooses its way.
KickConfig
interface KickConfig
The whole definition of a kick-block game. kickPreset supplies Pengo and the ice-slide rooms.
cols?: number— Columns of a generated board.rows?: number— Rows of a generated board.map?: string[]— An authored board: equal-width rows, characters looked up inlegend(unknown or space = floor).legend?: Record<string, KickLegendEntry>— Map characters.generator?: KickGenerator | null— Generate each level's board instead of (or when a level has no) map.materials: KickMaterial[]— Block kinds.enemies: KickEnemyKind[]— Enemy kinds.player: { /** Start cell for generated boards (a legend \player` cell wins on maps). Default: the even cell nearest the centre. / start?: Cell; /* Respawn candidates after a death, tried in order (cells with an egg or a special block are skipped). / respawn?: Cell[]; /* Cells per second. Pengo: 4.875. / speed: number; /* Seconds the push pose holds after a kick. / pushSeconds?: number; /* Seconds the player is busy breaking a block. / breakSeconds?: number; /* Seconds a kick is remembered before reaching a cell centre. */ kickBuffer?: number; }` — The player.slideSpeed?: number— Block slide speed, cells per second. Pengo: 16.25.shatterSeconds?: number— Seconds a block takes to shatter.chewSeconds?: number— Seconds a Sno-Bee needs to chew into a block (it creeps into the shattering cell). Pengo: about 0.72.oneSlide?: boolean— Only one block the player kicked may slide at a time (Pengo). Default true.hatchSeconds?: number— Seconds an egg takes to hatch, and the pause before the newcomer moves.hatchPauseSeconds?: number— Seconds a hatched enemy waits before moving.eggFlashSeconds?: number— Seconds eggs flash (in the enemy colour) at the start of a round and after each hatch.wallShake?: { seconds: number } | null— Kicking the border shakes it and stuns enemies on that edge. null disables.stun?: { seconds: number; blinkSeconds: number }— Stun length: still, then blinking. Pengo: about 1.6 s + 2.6 s.line?: { group: string; length: number; score: number; wallScore: number; pauseSeconds: number; stun: boolean; repeat?: boolean } | null— A line oflengthblocks ofgrouppaysscore(orwallScorealong the border), once per round unlessrepeat; then stuns every enemy.crushScores?: number[]— Points for crushing 1, 2, 3, 4… enemies with one slide (the last entry doubles beyond the table). Pengo: 400, 1600, 3200, 6400.stunnedScore?: number— Points for walking over a stunned enemy. Pengo: 100.eggScore?: number— Points for destroying an egg by breaking its block. Pengo: 500.timeBonus?: KickTimeBonus[]— Round time bonus table.fleeAllSeconds?: number | null— Seconds into a life after which every enemy runs for a corner (Pengo: 120). null disables.lastFleeSeconds?: number | null— Seconds the last enemy (no eggs left) lingers before running for a corner (Pengo: 15). null disables.fleeSpeed?: number— Speed of fleeing enemies (cells per second).dodgeSpeed?: number— Speed of an enemy dodging a slide.objective?: { enemies?: boolean; goals?: boolean }— What clears a round: every enemy and egg gone, every goal covered, or both. Default enemies only.crushPlayer?: boolean— Blocks kicked by enemies can crush the player. Default true.contact?: number— Contact distance (cells, per axis) between player and enemy. Default 0.5.lives?: number— Lives at the start.extraLives?: number[]— Scores that award an extra life (Pengo: 30,000 once).extraLifeEvery?: number— After the lastextraLivesentry, another life every this many points (0 = none).levels?: KickLevel[]— The level table.level?: number— Starting level (1-based).loop?: boolean— After the last level, start again at level 1 (Pengo loops after act 16). Default true.drawSeconds?: number— Seconds of the board-drawing intro (0 = none).readySeconds?: number— Seconds of READY before play.deathSeconds?: number— Seconds of the death animation.clearSeconds?: number— Seconds of the round-clear display.difficulty?: Partial<KickDifficulty>— Difficulty scaling. Default arcade (no scaling).seed?: number— Random seed (16-bit, the arcade generator).invulnerable?: boolean— Cannot die (for tests and attract modes).
KickDifficulty
interface KickDifficulty
Difficulty scaling over a level table. Arcade is all ones and zeros.
speed: number— Scale on every enemy speed.population: number— Scale on egg counts and authored enemy counts (rounded, at least 1 when the level has any).active: number— Added to the simultaneous-enemy cap (not below 1).patience: number— Scale on the hurry, flee and last-enemy timers (above 1 gives the player longer).
KickDifficultyName
type KickDifficultyName = "easy" | "normal" | "arcade"
Named difficulty presets. Arcade applies no scaling (the original tables).
KickEnemyKind
interface KickEnemyKind
An enemy kind.
id: string— Unique id; eggs and maps refer to it.brain?: KickBrain—sno-bee: Pengo's Sno-Bee (roaming, hunting along rows and columns, block-eating, dodging slides);patrol: walks straight and turns at obstacles;chase: shortest open path to the player;pusher: chases and kicks blocks at the player when lined up with one. Defaultsno-bee.breaksBlocks?: boolean— Breaks blocks in its way (Sno-Bees do). Default: true forsno-bee, else false.avoidsBlocks?: boolean— Dodges a block sliding toward it along its row or column. Default: true forsno-bee.stunnable?: boolean— Can be stunned by wall shakes and line bonuses. Default true.crushable?: boolean— Dies when a sliding block hits it. Default true.speedScale?: number— Scale on the level's enemy speed. Default 1.
KickEnemyMode
type KickEnemyMode = "roam" | "eat" | "hunt-x" | "hunt-y" | "hunt" | "dodge" | "flee"
A Sno-Bee mind state.
KickEnemyPhase
type KickEnemyPhase = | "hatching" | "pause" | "moving" | "stunned" | "carried" | "squashed" | "eaten" | "vanishing" | "gone"
An enemy's phase.
KickEnemyState
interface KickEnemyState
An enemy's visible state.
id: string— Stable id.kind: string— Enemy kind id.x: number— Continuous position.y: number— Continuous position.dir: Direction— Heading.phase: KickEnemyPhase— Phase.phaseTime: number— Seconds left in a timed phase.mode: KickEnemyMode— Mind state (sno-bee brain; other brains reportroamorhunt).chewing: boolean— True while chewing into a block.blinking: boolean— True during the blinking half of a stun.score: number— Points shown while squashed or eaten.carriedBy: number | null— The block carrying it, while carried.jump: number— Counts discontinuous jumps (spawn, respawn); a presenter snaps its sprite when this changes.
KickEvent
type KickEvent = | KickBoardEvent | { kind: "kick"; at: Cell; dir: Direction; result: string } | { kind: "shake"; side: Direction } | { kind: "stun"; id: string; cause: "shake" | "line" } | { kind: "crush"; ids: string[]; at: Cell; score: number } | { kind: "eat"; id: string; at: Cell; score: number } | { kind: "egg-broken"; at: Cell; score: number } | { kind: "hatch"; id: string; at: Cell } | { kind: "flee"; id: string } | { kind: "vanish"; id: string; at: Cell } | { kind: "line"; cells: Cell[]; alongWall: boolean; score: number } | { kind: "score"; points: number; at: Cell | null; reason: string } | { kind: "death"; at: Cell; cause: "enemy" | "block" } | { kind: "extra-life"; lives: number } | { kind: "hurry" } | { kind: "last-enemy" } | { kind: "round-clear"; time: number; bonus: number } | { kind: "level-start"; level: number } | { kind: "game-over" } | { kind: "complete" }
Something that happened, for sound and presentation.
KickGenerator
interface KickGenerator
A generated board, Pengo style.
kind: "ice-maze"— Pengo's ice maze.material: string— The material of the maze's blocks (and of egg blocks).egg: string— The enemy kind that hatches from generated eggs.specials?: { material: string; count: number }[]— Special blocks placed on odd cells after the eggs (Pengo: three diamond blocks).arcadeBug?: boolean— Reproduce the arcade generator's path-end bug. Default true.complete?: boolean— Keep carving until no ice pocket is sealed. Default false (one arcade pass).
KickLegendEntry
interface KickLegendEntry
What a map character places.
material?: string— A block of this material.egg?: string— The block hides an egg of this enemy kind.goal?: string— The floor is a goal for this block group.player?: boolean— The player starts here.enemy?: string— An enemy of this kind starts here (on the floor, not in a block).
KickLevel
interface KickLevel
A per-level patch. Every field is optional; level N uses entry min(N, length).
enemySpeed?: number— Enemy speed in cells per second.angrySpeed?: number— Enemy speed when fewer thanangryBelowremain (enemies also turn to block-eating then).angryBelow?: number— Remaining-enemy count (live plus eggs) below which enemies get angry. 0 disables.hurrySpeeds?: number[]— Enemy speed in each minute of a life after the first (index 0 = minute 1); enemies also turn to block-eating.eggs?: number— Eggs hidden in blocks when a board is generated.active?: number— Enemies on the board at once.map?: string[]— An authored map for this level (instead of the session map or generator).palette?: number— A presentation hint (Pengo's Sno-Bee colour changes every act).
KickMaterial
interface KickMaterial
A kind of block. Every field is optional; the defaults are Pengo's ice block.
id: string— Unique id, referenced by maps and legends.slides?: boolean— Kicking it makes it slide (true). False: it never moves (steel, a fixed pillar).maxSlide?: number— Cells it may travel in one kick (default unlimited); 1 makes a crate that moves one cell per kick.breaksWhenBlocked?: boolean— Kicking it while the cell beyond is not free damages it (true for ice).hits?: number— Kicks against an obstacle before it breaks. Default 1.enemyBreakable?: boolean— Enemies that break blocks may break it. Default: the same asbreaksWhenBlocked.crushes?: boolean— It kills crushable actors it slides into (true). False: it stops short of them.breakScore?: number— Points when the player breaks it. Default 0; Pengo's ice scores 30.group?: string— A group name for line-up bonuses and goals (Pengo's diamond blocks are group 'diamond').holdsEggs?: boolean— May hide an egg. Default: the same asbreaksWhenBlocked.
KickPlayerState
interface KickPlayerState
The player's visible state.
x: number— Continuous position in cells.y: number— Continuous position in cells.facing: Direction— The way the player faces (and kicks).moving: boolean— Moving this step.action: "walk" | "push" | "break" | "shake" | "dying"— What the player is doing.actionTime: number— Seconds left of the current action.jump: number— Counts discontinuous jumps (start, respawn); a presenter snaps its sprite when this changes.
KickPushResult
type KickPushResult = | { kind: "slide"; block: number } | { kind: "break"; block: number } | { kind: "crack"; block: number; damage: number } | { kind: "blocked"; block: number } | { kind: "busy"; block: number } | { kind: "empty" } | { kind: "border" }
What one KickBoard.push did.
KickSnapshot
interface KickSnapshot
A plain-data snapshot.
s: unknown— Session state (internal fields included).board: KickBoardSnapshot— Board.rng: number— Random seed.
KickState
interface KickState
A copy of the session state.
status: KickStatus— Session status.statusTime: number— Seconds in the current status.statusRemaining: number— Seconds left in a timed status (0 for playing).level: number— Level number (1-based; keeps counting through loops).act: number— The level-table row in force (1-based, wraps when looping): Pengo's ACT.score: number— Score.lives: number— Lives including the one in play.player: KickPlayerState— The player.enemies: KickEnemyState[]— Enemies, including ones in finishing phases (squashed, eaten, vanishing).blocks: KickBlockState[]— Every block.goals: { x: number; y: number; group: string; covered: boolean }[]— Goal cells.eggs: number— Eggs not yet hatched or destroyed.remaining: number— Live enemies plus eggs.eggFlash: boolean— True while egg blocks flash.shake: { side: Direction; remaining: number } | null— The border being shaken.line: { cells: Cell[]; score: number } | null— A line bonus being paid.roundTime: number— Round time (seconds, all lives) for the time bonus.lifeTime: number— Seconds into this life's play (drives hurry and flee).hurry: boolean— Enemies are hurrying.carveOrder: Cell[]— The order the generated board was carved (for a drawing intro); empty for maps.bonus: { time: number; score: number } | null— The last round's time bonus (shown duringclear).cols: number— Columns.rows: number— Rows.
KickStatus
type KickStatus = "drawing" | "ready" | "playing" | "bonus" | "dying" | "clear" | "lost" | "complete"
Status of the session.
KickTimeBonus
interface KickTimeBonus
Bonus for clearing a round quickly: the first entry whose under (seconds) exceeds the round time pays.
under: number— Round time must be below this many seconds.score: number— Points.
PengoOptions
interface PengoOptions
Options for the Pengo preset.
level?: number— Starting act (1–16).lives?: number— Lives (the DIP offers 2–5; default 3).seed?: number— Random seed.difficulty?: KickDifficultyName | Partial<KickDifficulty>— Difficulty scaling. Default arcade.skill?: number— The difficulty DIP (0–3). Default 1 (factory) for arcade, 0 for easy and normal.eggs?: "japan" | "export"— Egg table: the Japanese program (default) or the export sets.extraLives?: number[]— Extra-life score(s); the DIP offers 30,000 (default) or 50,000, once.cols?: number— Board size (odd); the arcade is 13 × 15.rows?: number— Board rows (odd); the arcade is 15.diamonds?: number— Diamond blocks (3 in the arcade; a line needslineLength).lineLength?: number— Diamonds in a line for the bonus. Default 3.arcadeBug?: boolean— Reproduce the maze generator's path-end bug. Default true.drawSeconds?: number— Seconds of the maze-drawing intro (the Japanese sets draw the maze; 0 skips it as the export sets do).


