The pack
bubbleshooter
function bubbleshooter(): Pack<{ bubbleshooter: BubbleshooterApi }>
The bubbleshooter pack. Pure logic, so it needs no backend and runs headless.
Helpers
BUBBLE_DIFFICULTY
const BUBBLE_DIFFICULTY: Readonly<Record<BubbleDifficultyName, BubbleDifficulty>>
Easy and Normal show the guide line and slow the press; Arcade is the original (guide only after a continue).
bubbleDifficulty
function bubbleDifficulty(name: BubbleDifficultyName = 'normal'): BubbleDifficulty
The difficulty table entry for a name.
ceilingInterval
function ceilingInterval(c: CeilingConfig, live: number): number
Shots between ceiling drops for live colours on the board.
createBubbleField
function createBubbleField(cfg: ShooterConfig): BubbleField
Creates a BubbleField from the given configuration.
createBubbleGame
function createBubbleGame(config: BubbleGameConfig): BubbleGame
Creates a session.
DEFAULT_LEGEND
const DEFAULT_LEGEND: Readonly<Record<string, LegendEntry>>
The default layout legend. Colours follow Puzzle Bobble's eight, in the order the pack numbers them:
B blue, Y yellow, R red, G green, P purple, O orange, K black, W white; digits 0-9 are colour indices too.
# clear (uncoloured) bubble, * star, X stone; ., _ and - are empty.
dropScore
function dropScore(n: number, base = 10, cap = 17): number
Puzzle Bobble's dropped-bubble bonus: 10 x 2^n for n bubbles dropped by one shot, n capped at cap (17).
exportRounds
function exportRounds(rounds: readonly BubbleRound[]): string
Export rounds in the importRounds format.
importRounds
function importRounds(text: string): BubbleRound[]
Import rounds from text. Rounds are separated by blank lines; a line starting with = names the round and may
carry cols=N for a wide board ("= Big one cols=17"). Other lines are layout rows (see parseLayout):
colour letters BYRGPOKW or digits, # clear, * star, X stone, . empty; spaces are ignored.
parseLayout
function parseLayout(rows: readonly string[], legend: Readonly<Record<string, LegendEntry>> = DEFAULT_LEGEND): [number, number, LegendEntry][]
Parse a layout: one string per row from the ceiling; whitespace is ignored, so rows may be written with the half-bubble indent of the screen. Returns [col, row, entry] for every occupied cell. Throws on unknown characters, naming the row and the character.
PUZZLE_BOBBLE_2_BOARDS
const PUZZLE_BOBBLE_2_BOARDS: Readonly<Record<string, readonly string[]>>
Puzzle Bobble 2 (Taito, 1995): the 135 boards of the alphabet tree, keyed by stage and round ("A-01"). Shapes, stones (X), uncoloured bubbles (#), stars (*) and colour counts from the level-map FAQ; colours authored. Wide boards are 17 bubbles across.
PUZZLE_BOBBLE_2_TIME_BONUS
const PUZZLE_BOBBLE_2_TIME_BONUS: TimeBonusConfig
Puzzle Bobble 2's: 500,000 within 5 s, falling linearly (about 11,850 a second, timed finer than a second), none from 48 s. The slope is fitted to the FAQ's sample table.
PUZZLE_BOBBLE_2_TREE
const PUZZLE_BOBBLE_2_TREE: readonly (readonly string[])[]
Puzzle Bobble 2's alphabet tree: two choices after every five rounds, six stages, seven endings.
PUZZLE_BOBBLE_CEILING
const PUZZLE_BOBBLE_CEILING: CeilingConfig
The press: with all eight colours on the board it comes down every baseShots shots, one shot sooner per missing
colour (FAQ). The base of 12 is an estimate that gives round 1 (four colours) a drop every eight shots.
PUZZLE_BOBBLE_COLOURS
const PUZZLE_BOBBLE_COLOURS: readonly { letter: string; name: string; monster: string }[]
Puzzle Bobble's eight bubble colours in pack order, with the Bubble Bobble monster inside each.
PUZZLE_BOBBLE_HURRY
const PUZZLE_BOBBLE_HURRY: HurryConfig
About ten seconds to fire, with a countdown for the last five (FAQ).
PUZZLE_BOBBLE_LAUNCHER
const PUZZLE_BOBBLE_LAUNCHER: LauncherConfig
The arcade launcher. Tilt range, turn speed, shot speed and reload are estimates from footage-era play.
PUZZLE_BOBBLE_ROUNDS
const PUZZLE_BOBBLE_ROUNDS: readonly (readonly string[])[]
Puzzle Bobble (Taito, 1994): the 30 rounds of the one-player game. Shapes and colour counts from the arcade FAQ's round diagrams; colours of rounds 1, 3 and 4 from arcade screenshots, the rest authored.
PUZZLE_BOBBLE_TIME_BONUS
const PUZZLE_BOBBLE_TIME_BONUS: TimeBonusConfig
Puzzle Bobble's round-clear bonus: 50,000 within 5 s, 840 less each further whole second, none from 65 s.
puzzleBobble2Preset
function puzzleBobble2Preset(opts: BubblePresetOptions = {}): BubbleGameConfig
Puzzle Bobble 2 (Taito, 1995): the alphabet-tree route (choose a stage after every five rounds), stone blocks, uncoloured bubbles that only drop, stars that clear a colour, wide 17-bubble boards, and the 500,000 time bonus. Same launcher, hurry and press as the first game (FAQ: "screen drop method is the same").
puzzleBobblePreset
function puzzleBobblePreset(opts: BubblePresetOptions = {}): BubbleGameConfig
Puzzle Bobble (Taito, 1994): 8 x 11 field above the line, 30 rounds, the arcade's scoring and timers.
pyramidRoute
function pyramidRoute(tiers: readonly (readonly string[])[], roundsPerStage: number, boards: Readonly<Record<string, readonly string[]>>): { rounds: BubbleRound[]; route: BubbleRoute; widths: Record<string, number> }
Build a route from a pyramid of stage ids: stage i of a tier leads to stages i and i+1 of the next tier. Each
stage's rounds are looked up as ${id}-${nn} in boards, numbered on from the tier's first round.
timeBonus
function timeBonus(cfg: TimeBonusConfig | null, seconds: number): number
Puzzle Bobble's time bonus formula from a config.
Classes
BubbleField
class BubbleField
An offset-hex bubble field hanging from a ceiling. Create one with createBubbleField.
readonly cols: number— Bubbles in a full row.readonly rows: number— Rows that fit above the dead line before any press.readonly colors: number— Palette size for random draws.readonly radius: number— The bubble radius, in world units.readonly rowHeight: number— Distance between row centres.readonly oddRows: 'short' | 'full'— Shifted-row policy.readonly hitScale: number— Contact distance as a fraction of a bubble's diameter.readonly popMin: number— Smallest group that pops.score: number— Legacy: score gained byshootso far.shots: number— Legacy: shots fired byshootthat hit the field.topOut: boolean— Legacy: true onceshoothas left a bubble over the line.constructor(cfg: ShooterConfig)shifted(y: number): boolean— True when rowyis drawn half a bubble to the right.rowWidth(y: number): number— Cells in rowy.inBounds(x: number, y: number): boolean— True when (x, y) is a cell of the grid (it may be empty).get press(): number— Ceiling presses so far.get ceilingY(): number— The world y of the ceiling (moves down a row per press).get lineRow(): number— The first row index that lies over the dead line.get width(): number— Field width in world units.get lineY(): number— World y of the dead line (fixed: the ceiling comes down to meet it).get height(): number— Legacy alias: the playfield height from the unpressed ceiling to the dead line.get launcher(): Vec— Where the launcher sits: centred, just below the line.centre(x: number, y: number): Vec— World centre of a cell (includes the press).at(x: number, y: number): Cell | null— The cell at (x, y), or null when empty or outside.bubble(x: number, y: number): number | null— Legacy: the colour at a cell (-1 for uncoloured kinds), or null when the cell is empty or outside.allocateId(): number— Reserve a fresh cell id (a session gives its flying shot the id the bubble will keep when it lands).cells(): CellView[]— Every occupied cell with its position, row by row.load(rows: readonly string[], legend: Readonly<Record<string, LegendEntry>> = DEFAULT_LEGEND): void— Replace the contents with a layout (seeparseLayout); the press and parity reset, ids carry on (never reused).set(x: number, y: number, kind: CellKind | null, color = 0): Cell | null— Set or clear one cell directly (editors, tests, garbage). Returns the new cell.neighbours(x: number, y: number): XY[]— Hex neighbours of (x, y): six, offset-aware.liveColors(): number[]— Colours of the coloured bubbles still in play. Sorted, so draws from it are deterministic.remaining(): number— Cells still to clear (everything except stones).get won(): boolean— True when nothing but stones is left.crossed(): boolean— True when a non-stone cell lies over the dead line.lowestRow(): number— The lowest occupied non-stone row, or -1 when empty: how close the field is to the line.trace(tilt: number, from?: Vec, maxBounces = 64): TraceResult— Trace a shot fromfrom(default the launcher) attiltradians from straight up (positive to the right, i.e. clockwise), reflecting off the side walls, until it touches a bubble or the ceiling. The shot is a circle: contact is exact (ray against circle), not a stepped walk.place(x: number, y: number, color: number, kind: CellKind = 'bubble', id?: number): Resolution— Put a shot into cell (x, y) and resolve it: a star it touches clears its colour; otherwise a same-colour group ofpopMinor more pops; then everything no longer hanging from the ceiling or a stone drops.dropOrphans(): CellView[]— Remove everything not connected to row 0 or to a stone; returns what fell.pressDown(): boolean— Lower the ceiling one row (Puzzle Bobble's press): every cell moves down with it. Returnscrossed().pushRow(colors: readonly (number | null)[]): boolean— Push a new row in from the top (rising-row modes): every row moves down one, parity flips, and row 0 is filled fromcolors(one entry per cell; null leaves a gap). Returnscrossed().snapshot(): FieldSnapshot— A plain-data snapshot.restore(s: FieldSnapshot): void— Restore a snapshot taken from a field with the same geometry.aim(tilt: number, launcher?: Vec): { path: Vec[]; hit: XY | null }— Legacy:tracefrom the launcher (orlauncher), returning only the path and the snap cell.shoot(tilt: number, color?: number): ShotResult— Legacy: fire a bubble ofcolor(or a live colour) attiltand resolve it instantly, scoring with the Puzzle Bobble table. A session (BubbleGame) is the timed, rule-complete alternative.findShots(samples = 60, colors?: readonly number[], range = 1.3): { angle: number; color: number; pops: number; drops: number; score: number; hit: XY }[]— Candidate shots scored by pops and drops, best first: a hint, and the brain of the bundled bots. Tilts are sampled evenly acrossrange(default ±1.3 rad). Coloured candidates usecolors(default the live colours). Never mutates the field.autoplay(maxShots = 300): ShotResult[]— Legacy: play best shots (scoring ones only) until cleared, crossed or stuck.debugGrid(): string— The field as text, one line per row (shifted rows indented): colour digits,#clear,*star,Xstone.
BubbleGame
class BubbleGame
A whole bubble-shooter session. Create one with createBubbleGame.
config: BubbleGameConfig— The configuration (rules may be edited between rounds throughconfigure).field: BubbleField— The field of the current round.constructor(config: BubbleGameConfig)turn(dir: number): void— Hold a turn direction: -1 left, 1 right, 0 none. Also moves the route highlight.setTilt(tilt: number): void— Set the tilt directly (pointer aiming), clamped to the launcher's range.aimAt(x: number, y: number): void— Aim at a world point (pointer aiming). Points below the launcher aim at the nearest limit.centre(): void— Point straight up.suggest(samples = 80): { tilt: number; pops: number; drops: number } | null— The best tilt for the loaded bubble (most drops, then most pops, then the best place to leave it), or null when nothing is loaded: an aim hint and the brain of an attract-mode or CPU player.fire(): void— Fire the loaded bubble (ignored while nothing is loaded); confirms the route choice; continues.chooseRoute(index: number): void— Choose a route stage by index of the offered choices.continueGame(): void— Take a continue during the countdown.goToRound(n: number): void— Jump to a 1-based round (debug, level select). Without a route only.skipRound(): void— Leave the current round unscored and go on as if it had been cleared (route rounds follow the route; a finished stage offers the next choice). For level select, practice and tests.configure(patch: Partial<Pick<BubbleGameConfig, 'launcher' | 'hurry' | 'ceiling' | 'scoring' | 'timeBonus' | 'guide' | 'continues' | 'rerollCleared'>>): void— Replace rule tables in a running session (launcher, hurry, ceiling, scoring, time bonus, guide).tick(dt: number): BubbleState— Advance bydtseconds in fixed 1/120 s steps. Returns the state.drain(): BubbleEvent[]— Events since the last drain.get state(): BubbleState— The current state (rebuilt lazily after each tick).snapshot(): BubbleSnapshot— A plain-data snapshot.restore(snap: BubbleSnapshot): void— Restore a snapshot taken from a session with the same configuration.
Types
BubbleDifficulty
interface BubbleDifficulty
Difficulty adjustments over the arcade numbers.
ceilingShots: number— Shots added to the ceiling interval.hurryScale: number— Multiplier on the hurry timer.guide: BubbleGameConfig['guide']— When the guide line shows.
BubbleDifficultyName
type BubbleDifficultyName = 'easy' | 'normal' | 'arcade'
Difficulty presets: arcade is the original's numbers; the others relax them.
BubbleEvent
type BubbleEvent = | { kind: 'round'; round: number; name: string; stage: string | null } | { kind: 'go' } | { kind: 'load'; color: number; next: number } | { kind: 'fire'; id: number; color: number; tilt: number; auto: boolean } | { kind: 'bounce'; x: number; y: number } | { kind: 'stick'; id: number; col: number; row: number; x: number; y: number } | { kind: 'pop'; cells: CellView[]; star: boolean } | { kind: 'drop'; cells: CellView[] } | { kind: 'score'; value: number; x: number; y: number; reason: 'pop' | 'drop' | 'time' } | { kind: 'warn'; shotsLeft: number } | { kind: 'press'; mode: 'press' | 'push' } | { kind: 'hurry'; secondsLeft: number } | { kind: 'clear'; time: number; bonus: number } | { kind: 'lost' } | { kind: 'continue'; left: number } | { kind: 'over' } | { kind: 'route'; choices: string[] } | { kind: 'complete' }
What the session reports through drain().
BubbleFieldConfig
interface BubbleFieldConfig
Field geometry for every round (see ShooterConfig for each option's meaning).
cols: number— Bubbles in a full row.rows: number— Rows above the dead line.radius: number— Bubble radius in world units.rowHeight?: number— Distance between row centres.hitScale?: number— Contact distance as a fraction of a diameter.oddRows?: 'short' | 'full'— Shifted-row policy.popMin?: number— Smallest same-colour group that pops.lineGap?: number— Space between the lowest legal row and the dead line.launcherDrop?: number— Launcher centre below the line.
BubbleGameConfig
interface BubbleGameConfig
Everything a session needs. Presets build one (puzzleBobblePreset, puzzleBobble2Preset).
name?: string— Display name of the ruleset.field: BubbleFieldConfig— Field geometry.palette: number— Colours in the palette (0 .. palette-1).rounds: BubbleRound[]— The rounds, in order (or referenced byroute).route?: BubbleRoute— A branching route overrounds; without one the rounds are played in order.startRound?: number— 1-based round to start from (ignored with a route). Default 1.loop?: boolean— After the last round, start again from the first instead of finishing. Default false.legend?: Readonly<Record<string, LegendEntry>>— Layout characters. DefaultDEFAULT_LEGEND.launcher: LauncherConfig— The launcher.hurry: HurryConfig | null— The idle timer, or null for none.ceiling: CeilingConfig— The ceiling.scoring: ScoringConfig— Points.timeBonus: TimeBonusConfig | null— The time bonus, or null for none.guide: 'always' | 'continue' | 'never'— When the aim guide (the traced path) is shown: always, only for the round after a continue, or never.continues: number— Continues allowed (-1 for unlimited).continueScore: 'reset' | 'keep'— A continue restarts the round with the score reset (Puzzle Bobble) or kept.rerollCleared?: boolean— Re-deal a loaded or next bubble whose colour has just left the board. Default true.timings: BubbleTimings— Phase timings.seed?: number— Seed for the colour deal.difficulty?: BubbleDifficultyName— The difficulty the config was built for (informational).
BubblePresetOptions
interface BubblePresetOptions
Options shared by the presets.
difficulty?: BubbleDifficultyName— Easy, Normal (default) or Arcade.startRound?: number— 1-based start round (Puzzle Bobble only).seed?: number— Seed for the colour deal.rounds?: BubbleRound[]— Replace the rounds (e.g. fromimportRounds).continues?: number— Continues (-1 unlimited, the default: a continue is a coin).
BubbleRound
interface BubbleRound
One authored round: a layout and, optionally, its own width.
name?: string— Display name (e.g. "ROUND 1", "A-3"). Default "ROUND n".layout: readonly string[]— Layout rows from the ceiling (seeparseLayout).cols?: number— Bubbles in a full row for this round (Puzzle Bobble 2's wide boards use 17). Defaultfield.cols.
BubbleRoute
interface BubbleRoute
A branching route: the player picks a stage at the start and after each stage.
start: string[]— The stage ids offered at the start.stages: RouteStage[]— Every stage.
BubbleshooterApi
type BubbleshooterApi = typeof kit
game.bubbleshooter — every function and type this pack carries, as one namespace.
BubbleSnapshot
interface BubbleSnapshot
A plain-data snapshot of a session.
s: Record<string, unknown>— Session scalars.field: FieldSnapshot— The field.cols: number— Field columns (rounds may change width).rng: RngState— Where the seeded stream stands (a kernelRngsnapshot).
BubbleState
interface BubbleState
The read-only session state, rebuilt after each tick.
status: BubbleStatus— Phase.statusTime: number— Seconds in this phase.round: number— Rounds played in this run, 1-based (the number on screen).roundIndex: number— Index intoroundsof the round on the field.roundName: string— The round's name.stage: string | null— Route stage id, or null without a route.score: number— Score.continuesUsed: number— Continues used.width: number— Field width (world units) for this round.cols: number— Columns for this round.rows: number— Rows above the line.radius: number— Bubble radius.ceilingY: number— World y of the ceiling (animated during a press).lineY: number— World y of the dead line.bubbles: BubbleView[]— Every bubble on the field.shot: { id: number; color: number; x: number; y: number } | null— The flying shot, or null.launcher: { x: number; y: number; tilt: number; maxTilt: number }— Launcher centre and tilt (radians from straight up, positive to the right).current: number | null— Colour in the launcher, or null while reloading.next: number | null— Colour waiting in the bag.loaded: number— 0 while the next bubble is being loaded, rising to 1 when it can fire.shots: number— Shots fired this round.shotsToDrop: number— Shots left before the ceiling comes down (Infinity when it never does).shake: number— 0 still, 1 shaking, 2 shaking hard: the warning before a drop.hurry: number | null— Seconds left on the HURRY UP countdown, or null when it is not showing.roundTime: number— Seconds since the round started.guide: Vec[] | null— The guide path when shown.lastBonus: number— The bonus paid at the last clear.lastClearTime: number— The time of the last clear.route: { choices: string[]; selected: number } | null— Route choices while choosing, with the highlighted index.continueLeft: number— Seconds left on the continue countdown.danger: number— How full the field is: lowest occupied row / rows above the line (0..1); presenters speed music with it.
BubbleStatus
type BubbleStatus = 'route' | 'ready' | 'playing' | 'clear' | 'lost' | 'continue' | 'over' | 'complete'
Session phase.
BubbleTimings
interface BubbleTimings
Banner and phase timings, seconds.
ready: number— Round banner and READY before play starts.clear: number— CLEAR banner and bonus before the next round.lost: number— Lost animation before the continue countdown.continueSeconds: number— The continue countdown.route: number— Route choice before the highlighted stage is taken anyway.
BubbleView
interface BubbleView
A bubble as the presenter sees it: stable id, kind, colour and position (press animation applied).
id: number— Stable id: one sprite per id.kind: CellKind— Cell kind.color: number— Colour (-1 for uncoloured kinds).col: number— Column.row: number— Row.x: number— World x.y: number— World y.
CeilingConfig
interface CeilingConfig
The ceiling schedule.
mode: 'press' | 'push' | 'none'—press: the ceiling and everything on it come down a row (Puzzle Bobble).push: a new row of live colours is pushed in from the top (rising-row variants).none: never.baseShots: number— Shots between drops when every palette colour is on the board.palette: number— The palette size counted as "every colour".perMissingColour: number— Shots taken off the interval per palette colour missing from the board.minShots: number— The shortest interval.warnShots: number— The board shakes when this many shots or fewer remain before a drop (0 disables).dropSeconds: number— Seconds the press takes to come down (no firing meanwhile).
Cell
interface Cell
One occupied cell. id is stable for the cell's whole life, so a presenter can key one sprite to it.
id: number— Stable id, unique within the field.kind: CellKind— What the cell holds.color: number— The colour index forbubble; -1 for every other kind.
CellKind
type CellKind = 'bubble' | 'clear' | 'stone' | 'star'
What a cell holds.
bubble: a coloured bubble. Pops in a same-colour group ofpopMinor more; drops when cut loose.clear: an uncoloured bubble (Puzzle Bobble 2's jama bubble). Never pops; only drops.stone: a fixed block (Puzzle Bobble 2's jama block). Never pops, never drops, and anchors whatever hangs from it as the ceiling does. Ignored by the clear test and the dead line.star: Puzzle Bobble 2's gem. A coloured shot that lands touching it pops every bubble of the shot's colour and the star. Otherwise it behaves as a clear bubble.
CellView
interface CellView extends Cell
A cell together with where it is: what queries, pops and drops report.
col: number— The column.row: number— The row.x: number— The world centre x.y: number— The world centre y (includes the ceiling press).
FieldSnapshot
interface FieldSnapshot
A plain-data snapshot of a field.
cells: [number, number, number, CellKind, number][]— Occupied cells as [col, row, id, kind, color].press: number— Ceiling presses so far.parity: number— Row parity offset (toggled bypushRow).nextId: number— Next cell id.score: number— Legacy counters.shots: number— Legacy counters.rng: RngState— Where the seeded stream stands (a kernelRngsnapshot).
HurryConfig
interface HurryConfig
The idle timer: a warning countdown, then the launcher fires by itself.
warnAfter: number— Seconds after a bubble is loaded before the HURRY UP countdown shows.fireAfter: number— Seconds after a bubble is loaded when it fires automatically at the current tilt.
LauncherConfig
interface LauncherConfig
The launcher ("pointer").
maxTilt: number— Largest tilt either side of straight up, in radians.turnSpeed: number— Turn speed while a direction is held, radians per second.shotSpeed: number— Shot speed, world units per second.reloadSeconds: number— Seconds after a shot lands before the next bubble is loaded and can fire.
LegendEntry
type LegendEntry = { … }
How a layout character maps to a cell. Colour letters map to { kind: 'bubble', color }.
kind: CellKindcolor?: number
Resolution
interface Resolution
What place did: the bubble that landed, what popped, what fell.
placed: CellView— The placed cell as it landed (it is also inpopswhen it popped).pops: CellView[]— Cells cleared by the pop (the placed bubble included), or by a star. Empty when nothing popped.drops: CellView[]— Cells that fell because they no longer hang from the ceiling or a stone.star: boolean— True when a star did the popping.
RouteStage
interface RouteStage
A stage of a branching route (Puzzle Bobble 2's alphabet tree).
id: string— Stage id shown on the map ("A", "B", "?").rounds: number[]— Indices intorounds, played in order.next: string[]— Stage ids the player may choose after this one; empty for a final stage.
ScoringConfig
interface ScoringConfig
Points.
pop: number— Points per popped bubble.dropBase: number— Dropped bubbles scoredropBase * 2^nfor n dropped by one shot.dropCap: number— n is capped here (17 in Puzzle Bobble: 1,310,720).
ShooterConfig
interface ShooterConfig
Configures a BubbleField (and, through createBubbleField, the legacy instant-resolution board).
cols: number— Bubbles in a full row. Puzzle Bobble: 8 (17 on Puzzle Bobble 2's wide boards).rows: number— Rows that fit above the dead line. A bubble that comes to rest in rowrowsor below (after presses) has crossed the line. Puzzle Bobble: 11.seed?: number— Seed for colour draws made by the field itself (legacyshoot,fillRows,pushRow). Default 1.colors?: number— Palette size used by random draws. Default 6.radius?: number— Bubble radius in world units. Default 16 (the Puzzle Bobble presets use 8, the arcade's 16-pixel bubble).rowHeight?: number— Distance between row centres. Defaultradius * 1.75(14 for the arcade's 8).oddRows?: 'short' | 'full'—short: shifted rows holdcols - 1bubbles and the field iscolsbubbles wide (Puzzle Bobble).full: every row holdscolsand the field is half a bubble wider. Defaultshort.hitScale?: number— A shot stops when its centre comes within2 * radius * hitScaleof a bubble's centre. Below 1 lets a well-aimed shot slip into a one-bubble gap, as the arcade does. Default 0.8.lineGap?: number— Extra space between the lowest legal row and the drawn dead line. Defaultradius / 4.launcherDrop?: number— Launcher centre below the dead line. Defaultradius * 1.25.layout?: readonly string[]— Initial layout, one string per row from the ceiling (seeparseLayout).legend?: Readonly<Record<string, LegendEntry>>— Characters forlayout. DefaultDEFAULT_LEGEND.fillRows?: number— Legacy: pre-fill this many top rows with random colours when no layout is given. Default 0 with a layout, otherwise 5.ceilingEveryShots?: number— Legacy instantshoot: push a new random row in from the top after this many shots. 0 disables. Default 0.popMin?: number— Smallest same-colour group that pops. Default 3.
ShotResult
interface ShotResult
The outcome of one legacy BubbleField.shoot call.
path: Vec[]— World-space flight path (launcher to snap), reflecting off walls.snap: XY | null— The cell the shot bubble snapped to, or null when the shot missed.pops: XY[]— The cells cleared by the same-colour pop. Empty when the group was smaller thanpopMin.drops: XY[]— The cells that fell because the pop cut them off from the ceiling.ceilingStepped: boolean— True when this shot pushed a new row in from the top (ceilingEveryShots).topOut: boolean— True when a bubble has crossed the dead line: the game is lost.won: boolean— True when the field is now empty.gained: number— The score gained by this shot (Puzzle Bobble table: 10 a pop, 10 x 2^n for n dropped, n capped at 17).
TimeBonusConfig
interface TimeBonusConfig
The round-clear time bonus: full until fullUntil seconds, then falling by perSecond, none from cutoff.
max: number— The full bonus.fullUntil: number— Seconds within which the full bonus is paid.perSecond: number— Points lost per second afterfullUntil.cutoff: number— No bonus at or after this many seconds.wholeSeconds: boolean— Count whole seconds (Puzzle Bobble) or exact time (Puzzle Bobble 2).step?: number— Round the bonus down to a multiple of this. Default 10.
TraceResult
interface TraceResult
The outcome of BubbleField.trace: where a shot goes and what it touches.
path: Vec[]— World-space path: the start, each wall bounce, and the contact point.hit: XY | null— The cell the shot would snap into, or null when it never touches anything.contact: XY | null— The bubble it touched, or null for the ceiling (or a miss).bounces: number— Number of wall bounces on the way.
Vec
interface Vec
A world-space point, in field units (y down, origin at the ceiling's left end).
x: number— The x position, in world units.y: number— The y position, in world units.
XY
interface XY
A grid cell position on the field: column x, row y counted down from the ceiling.
x: number— The column of the cell.y: number— The row of the cell (0 hangs from the ceiling).


