Phaser AE

Match-3 boards

Pro packv3.0.0-32DGrid

A tile-matching board that resolves every move at once into an ordered journal of steps, which your game replays at its own pace. Specials, combos, obstacles, shaped boards, goals and several styles of play are all the same board with different rules turned on.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 14, 2026 · updated October 20, 2026

Match-3 boards: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

match3

function match3(): Pack<{ match3: Match3Api }>

The match3 pack. Pure logic, so it needs no backend and runs headless.

Helpers

classifyGroup

function classifyGroup(group: MatchGroup, pivotHint?: XY): { shape: MatchShape; orientation: 'h' | 'v' | 'both' | 'none'; pivot: XY }

Classify a group's shape by the standard priority: 5-line > L/T/cross > square > 4 > 3.

createBoard

function createBoard(cfg: BoardConfig): Board

Create a board. See BoardConfig for every option.

findMatchGroups

function findMatchGroups(grid: MatchScanGrid, squareMatch = false): MatchGroup[]

Scan the whole grid: all horizontal + vertical runs of 3+, merged into groups. Optionally also standalone 2×2 squares (cells not already in any run).

floodGroup

function floodGroup(grid: MatchScanGrid, x: number, y: number, adj8 = false): XY[]

Flood-fill the connected same-colour group containing (x, y). Orthogonal by default; adj8 includes diagonal neighbours (8-way chains).

Classes

Board

class Board

The headless match-3 board model: grid, moves, cascades, score and goals. Create one with createBoard().

  • readonly cols: number — The number of columns, from the config.
  • readonly rows: number — The number of rows, from the config.
  • readonly preset: 'match3' | 'collapse' | 'chain' — The rule preset the board plays: 'match3' (swap), 'collapse' (tap groups) or 'chain' (drag lines).
  • readonly colors: number — How many block colors the board deals from.
  • score: number — Cumulative score.
  • moveCount: number — Valid moves made so far.
  • movesLeft: number — Remaining move budget when moveLimit was configured, else Infinity.
  • goals: GoalState[] — Live goal counters (empty when no goals configured).
  • constructor(cfg: BoardConfig)
  • snapshot(): BoardSnapshot — The board's whole state as plain data: cells, blocks, overlays, goals, score, the move budget and the seeded stream. restore it into a board built from the same config, for undo, a save or a search that tries a move and takes it back. Presentation in progress (beginPresent) is not part of it.
  • restore(snap: BoardSnapshot): void — Return to a snapshot() of a board built from the same config.
  • cell(x: number, y: number): Cell — The cell at (x, y). Mutating it mutates the board's substrate.
  • block(x: number, y: number): Block | null — The block at (x, y), or null.
  • overlay(x: number, y: number): OverlayEntry | null — The overlay at (x, y), or null.
  • setBlock(x: number, y: number, init: BlockInit | null): Block | null — Place (or clear, with null) a block during level setup.
  • setOverlay(x: number, y: number, entry: OverlayEntry | null): void — Attach an overlay (chain/vine/cage…) during level setup.
  • addLayer(x: number, y: number, layer: CellLayer): void — Add a damageable cell layer (jelly…) during level setup.
  • forEach(fn: (x: number, y: number, cell: Cell, block: Block | null, overlay: OverlayEntry | null) => void): void — Visit every cell.
  • debugGrid(): string — Rows of colour digits (. empty, # blocked, ~ conduit, * colourless) — handy in tests.
  • swap(a: XY, b: XY): MoveResult — Swap two adjacent blocks. Instant full resolution; play back result.steps.
  • rotate(at: XY, dir: 'cw' | 'ccw' = 'cw'): MoveResult — Rotate the 2×2 block whose top-left is at, 'cw' or 'ccw' (Bejeweled Twist). Reverts if it makes no match, unless operatorFreeMove.
  • shiftRow(row: number, by: number): MoveResult — Slide a whole row by by cells with wraparound (Chuzzle / 10000000).
  • shiftColumn(col: number, by: number): MoveResult — Slide a whole column by by cells with wraparound.
  • tap(at: XY): MoveResult — Tap a cell: collapse preset clears the flood group; a tapped special fires.
  • pathAt(cells: XY[]): { valid: boolean; loop: boolean; color: number; cells: XY[] } — Chain preset: validate a drawn path without playing it. A path is valid when every cell holds a matchable block of ONE colour(+tier), consecutive cells are adjacent (see chain.adjacency), no cell repeats — except that the FINAL cell may revisit an earlier one, closing a loop — and the unique length reaches chain.minLength. Use per-step while the player drags.
  • playPath(cells: XY[]): MoveResult — Chain preset: commit a drawn path. A closed loop clears EVERY block of the path's colour (when chain.loopClearsColor isn't false).
  • applyAction(action: Action): MoveResult — Apply an inventory power-up / tool action. Cascades follow; does not consume a move.
  • endTurn(): MoveResult — Run the turn systems (conveyors, spreaders, bomb fuses) manually when autoTick: false.
  • findMoves(): PredictedMove[] — Every legal move, with its predicted primary outcome, best first.
  • hint(): PredictedMove | null — The single best move, or null.
  • autoplay(opts: AutoplayOptions = {}): MoveResult[] — Play the board by itself: pick a move per policy, apply it, repeat. Stops on maxMoves, no moves left, all goals done, or a lose trigger.
  • beginPresent(result: MoveResult): void — Mark a result's region as visually busy while the game plays it back.
  • endPresent(result: MoveResult): void — Release a region when its playback finishes.
  • busy(): number[] — Cell indices currently visually busy.
  • canMove(a: XY, b?: XY): boolean — Conservative admission for parallel play: false when the COLUMNS of the candidate cells intersect any busy region (cascades pull whole columns).

Types

Action

type Action = | { type: 'clearRow'; row: number } | { type: 'clearColumn'; col: number } /** Square blast centred at `at`; `radius: 1` = 3×3. */ | { type: 'blast'; at: XY; radius: number } | { type: 'clearColor'; color: number } | { type: 'paint'; at: XY; color: number } /** Clear one block (or one overlay/blocker layer) at a chosen cell. */ | { type: 'hammer'; at: XY } /** Hit `count` random block cells within `radius` of `at`. */ | { type: 'splat'; at: XY; radius: number; count: number } | { type: 'randomClear'; count: number } | { type: 'shuffle' } /** Swap two adjacent cells with no match requirement (the "glove"). */ | { type: 'freeSwap'; a: XY; b: XY } /** Convert every block of `color` into `special`, then fire them all. */ | { type: 'convertAndFire'; color: number; special: SpecialDef } | { type: 'custom'; cells: (ctx: { cols: number; rows: number }) => XY[] }

A grid mutation. Applied via board.applyAction, special blocks, or combos.

AutoplayOptions

interface AutoplayOptions

Options for Board.autoplay.

  • policy?: 'best' | 'random' | 'worst' | ((moves: PredictedMove[], board: Board) => PredictedMove) — Move policy. Default 'best'. A function receives the sorted move list.
  • maxMoves?: number — Stop after this many moves. Default 100.
  • onMove?: (result: MoveResult, move: PredictedMove) => void — Called after each move with its result.

Block

interface Block extends Required<Pick<BlockInit, 'color' | 'kind'>>

A live block on the board. Identity (id) is stable across its whole life.

  • id: number — Stable id — key your sprites off this, never off grid position.
  • special?: SpecialDef | undefined — The special payload, when this is a special block.
  • hp: number — Matches remaining before the block clears.
  • matchable: boolean — True when the block can take part in a match.
  • static: boolean — True when the block never falls.
  • hitFilter?: HitType[] | undefined — Which hit types damage a blocker block.
  • bombTimer?: number | undefined — Moves remaining on the fuse. Reaching 0 fires a lose trigger.
  • tags?: string[] | undefined — Free-form game labels, carried through every journal event.
  • tier: number — The merge-mode level (0-based).

BlockInit

interface BlockInit

Everything you can specify when creating/placing a block.

  • color?: number — Colour index 0..colors-1, or -1 for colourless (ingredients, crates).
  • kind?: BlockKind — What the block fundamentally is. Default 'normal'.
  • special?: SpecialDef | undefined — The special payload. Makes the block a special when set.
  • hp?: number — Matches needed to clear it. 1 (default) clears on first match; ice-style blocks use 2+ — each match cracks a layer (blockDamage events) until 0.
  • matchable?: boolean — false → never part of a match (ingredients, crates). Default from kind.
  • static?: boolean — true → never falls (meringue-style anchored blockers).
  • hitFilter?: HitType[] | undefined — Which hit types damage a blocker block. Default ['adjacent','blast','beam','tool'].
  • bombTimer?: number — Move-countdown fuse: ticks down once per move; reaching 0 fires a lose trigger.
  • tags?: string[] | undefined — Free-form game labels, carried through every journal event.
  • tier?: number — Merge-mode level (0-based). Blocks only match with the SAME colour + tier.

BlockKind

type BlockKind = 'normal' | 'special' | 'ingredient' | 'blocker'

What a block fundamentally is.

BlockSnapshot

type BlockSnapshot = Readonly<Block>

An immutable copy of a block as it was when the event fired.

BoardConfig

interface BoardConfig

Board construction options.

  • cols: number — The number of board columns.
  • rows: number — The number of board rows.
  • colors?: number — Number of block colours dealt (0..colors-1). Default 6.
  • seed?: number — PRNG seed. Same seed + same moves = identical journals. Default 1.
  • preset?: 'match3' | 'collapse' | 'chain' — match3 (default): adjacent swap must make a match; line matcher; cascades re-match. collapse: tap a flood group of minGroup+ same colour; no refill; empty columns close up horizontally; cascades do NOT auto-match. chain: the player draws a path through adjacent same-colour blocks (board.playPath); refills from the top; cascades do NOT auto-match.
  • chain?: { /** Path step adjacency. Default 4 (orthogonal); 8 allows diagonals. */ adjacency?: 4 | 8; /** Minimum path length. Default 3. */ minLength?: number; /** Closing the path into a loop clears EVERY block of that colour. Default true. */ loopClearsColor?: boolean; } — Chain preset tuning.
  • merge?: { maxTier: number; explode?: boolean; style?: 'fuse' | 'levelAll' } — Merge resolution: matches LEVEL blocks UP instead of clearing them. Reaching maxTier EXPLODES: the block clears and every neighbour (3×3) levels up — neighbours that hit maxTier explode too, chain-reacting. Shape specials are disabled in merge mode. Two styles: - 'fuse' (default, Triple-Town): blocks only match EQUAL colour+tier; the group is consumed into ONE next-tier block at the pivot; cascades re-match as normal. - 'levelAll': ANY same-colour trio matches regardless of tier; nothing is consumed — EVERY participant gains a tier in place. Only the group(s) containing the swapped cells resolve, and cascades never auto-match (levelled triples persist on the board); explosions are the only exit.
  • layout?: string[] — The board shape, one string per row. . normal · # blocked (hole) · ~ conduit · < > ^ v conveyor · E exit cell (normal + exit). Omit for an all-normal rectangle.
  • minGroup?: number — Collapse preset: minimum flood-group size. Default 2.
  • squareMatch?: boolean — Recognise 2×2 squares as a match shape. Default false.
  • refill?: 'top' | 'none' — Refill from the top after clears. Defaults: match3 'top', collapse 'none'.
  • diagonalSlide?: boolean — Blocks slide diagonally into cells that cannot be fed vertically (starved by holes/static blocks). Default: true for match3, false for collapse.
  • horizontalCollapse?: boolean — Collapse preset: empty columns close toward the left. Default true (collapse only).
  • spawnWeights?: SpawnWeights — Colour weights for fill/refill. Default uniform.
  • spawnTable?: { weight: number; block: BlockInit | (() => BlockInit) }[] — Weighted deal/refill CONTENT — when present it replaces spawnWeights: every dealt/refilled block picks an entry by weight (Puzzle-Quest skulls, cannon-fed blockers, rare bombs…). Entries may be factories for per-spawn variation. The initial deal still guarantees a no-match, movable board.
  • specialSpawns?: Partial<Record<MatchShape, SpecialDef | null>> — Shape → special spawned at the pivot. null disables one shape. Default: four → row/colClear (perpendicular to the run), five/longer → colorClear, L/T/cross → blast r1, square → seeker (if squareMatch). Collapse preset instead uses collapseSpecials.
  • collapseSpecials?: Record<number, SpecialDef> — Collapse preset: group-size thresholds → special left at the tap cell. Default: 5 → row/colClear, 7 → blast, 9 → colorClear. {} disables.
  • comboMatrix?: Record<string, (a: XY, ctx: { colorA: number; colorB: number }) => Action[]> — Override/extend the special+special swap combo matrix. Keys like 'blast+rowClear' (kinds sorted alphabetically, joined with +).
  • tapActivatesSpecials?: boolean — Tapping a special block on the board fires it (Royal Match style). Default true.
  • swapAnywhere?: boolean — Swap any two cells, not just adjacent ones (Pokémon Shuffle). Default false.
  • operatorFreeMove?: boolean — A rotate/shiftRow/shiftColumn move need not create a match to be legal (Bejeweled Twist). Default false — the operator reverts if it makes no match.
  • autoTick?: boolean — Auto-append the turn tick (conveyors, spread, bomb fuses) to every valid move. Default true; set false and call board.endTurn() yourself.
  • autoShuffle?: boolean — Auto-shuffle when no legal move exists after a move settles. Default true (match3).
  • goals?: GoalDef[] — The level goals. Progress arrives in MoveResult.goalsDelta; live state is on board.goals.
  • moveLimit?: number — Optional move budget — board.movesLeft counts down; purely informational.
  • scorer?: (ev: { kind: 'clear' | 'match'; blocks: number; shape?: MatchShape; depth: number }) => number — Replace the default scorer. depth is the cascade round (0 = direct).

BoardSnapshot

interface BoardSnapshot

A board's state from snapshot(): plain data (movesLeft may be Infinity).

  • cells: Cell[] — The cells, row by row.
  • grid: (Block | null)[] — The blocks, row by row.
  • overlays: (OverlayEntry | null)[] — The overlays, row by row.
  • goals: GoalState[] — Goal counters.
  • score: number — Score.
  • moveCount: number — Valid moves made.
  • movesLeft: number — Moves left in the budget.
  • nextId: number — The next block id.
  • rng: RngState — Where the seeded stream stands.

Cell

interface Cell

One board square. All fields optional except kind; a plain cell is { kind: 'normal' }.

  • kind: CellKind — What the square fundamentally is. See CellKind.
  • flow?: Dir[] — Fall routing out of this cell, tried in order. Default ['down']. Diagonal slide past blocked cells is automatic (see BoardConfig.diagonalSlide); set flow only for custom routing.
  • conveyor?: Dir — Belt direction for kind: 'conveyor' cells. Belts chain into runs/loops.
  • portal?: { to: XY } — A block that lands here is teleported to to and keeps falling from there.
  • spawner?: boolean — Marks a refill entry point. By default every top-row non-blocked cell spawns.
  • exit?: boolean — Bottom slot: a non-matchable ingredient block resting here exits and counts.
  • layers?: CellLayer[] — Damageable under-layers (jelly etc.), topmost last.

CellKind

type CellKind = 'normal' | 'blocked' | 'conduit' | 'conveyor'

What a board square fundamentally is:

  • normal — holds a block.
  • blocked — a hole/dead cell: never holds a block, stops falls (carves the board shape out of the rectangle).
  • conduit — holds nothing, but falling blocks pass straight through it (the "invisible gap rows" between board sections).
  • conveyor — holds a block and shifts its content one step in conveyor direction on each turn tick.

CellLayer

interface CellLayer

A damageable layer painted ON the cell itself (under the block): jelly, gold-gilding, grass. Damaged by hits that land on the cell; removed at hp 0.

  • kind: string — Game-defined label, e.g. 'jelly'. Goals can target it.
  • hp: number — Hits remaining before the layer clears. Double jelly = 2.
  • hitFilter?: HitType[] — Which hit types damage it. Default: every direct hit (not adjacent).

Dir

type Dir = 'up' | 'down' | 'left' | 'right' | 'downLeft' | 'downRight'

A direction on the board grid. downLeft/downRight appear in flow routing.

FallMove

interface FallMove

A block displacement within a fall step. path includes every cell traversed (conduit/portal waypoints included) — tween through it, or teleport past it.

  • blockId: number — The stable id of the block that moved.
  • from: XY — The cell the block left.
  • to: XY — The cell the block came to rest in.
  • path: XY[] — Every cell the block traversed, in order. Tween through it, or teleport past it.

GoalDef

type GoalDef = | { id?: string; type: 'score'; count: number } | { id?: string; type: 'clearColor'; color: number; count: number } /** Count cleared blocks carrying this tag (crates, gems…). */ | { id?: string; type: 'clearTagged'; tag: string; count: number } | { id?: string; type: 'clearLayer'; kind: string; count: number } | { id?: string; type: 'clearOverlay'; kind: string; count: number } | { id?: string; type: 'ingredients'; count: number } | { id?: string; type: 'fireSpecials'; count: number }

A level goal, wired to journal events.

GoalDelta

interface GoalDelta

A goal counter changing as a consequence of a move.

  • id: string — The id of the goal that changed.
  • amount: number — How much this move progressed it.
  • remaining: number — The count still to go after this move.
  • done: boolean — True when this move completed the goal.

GoalState

interface GoalState

Live goal state, readable on board.goals.

  • id: string — The goal id (from the definition, or generated).
  • def: GoalDef — The goal definition this state tracks.
  • target: number — The total count the goal asks for.
  • remaining: number — The count still to go. 0 when the goal is complete.
  • done: boolean — True when the goal is complete.

HitType

type HitType = 'match' | 'adjacent' | 'blast' | 'beam' | 'tool'

Every clearing event lands as typed hits:

  • match — on the matched cells themselves
  • adjacent — splash onto orthogonal neighbours of a clear (how crates die)
  • blast — bomb/area explosions
  • beam — row/column clears
  • tool — inventory power-ups (hammer etc.) Cell layers, overlays and blocker blocks each declare which types damage them.

Match3Api

type Match3Api = typeof api

game.match3 — every function and class this pack carries, as one namespace.

MatchEvent

interface MatchEvent

One resolved match, with everything the game needs to know about it.

  • shape: MatchShape — The recognised match shape.
  • length: number — Total blocks involved.
  • orientation: 'h' | 'v' | 'both' | 'none' — The run direction: horizontal, vertical, both (L/T/cross), or none (group/square).
  • color: number — The colour index of the matched blocks.
  • cells: XY[] — Every cell in the match.
  • blocks: BlockSnapshot[] — Every block in the match, snapshotted at match time.
  • pivot: XY — The moved/tapped cell (specials spawn here); run intersection for cascades.
  • overlays: { at: XY; kind: string; hp: number }[] — Overlays present on matched cells at match time.
  • layers: { at: XY; kind: string; hp: number }[] — Cell layers present under matched cells at match time.
  • chainDepth: number — 0 = made directly by the move; 1+ = cascade round that produced it.
  • cause: 'move' | 'cascade' | 'action' — What produced the match: the player's move, a cascade, or an applied action.

MatchGroup

interface MatchGroup

A merged match group (runs sharing a cell fuse into one match).

  • color: number — The match color shared by every cell in the group.
  • cells: XY[] — The unique cells in the group, from all merged runs.
  • runs: Run[] — The runs that merged into this group. Empty for a standalone square.
  • square: boolean — True when this group is a standalone 2×2 square (no runs).

MatchScanGrid

interface MatchScanGrid

Minimal grid view the scanner needs. Return -2 for empty/unmatchable cells.

  • cols: number — The number of columns in the grid.
  • rows: number — The number of rows in the grid.
  • matchColorAt(x: number, y: number): number — Returns the match color at a cell, or -2 for an empty or unmatchable cell.

MatchShape

type MatchShape = 'three' | 'four' | 'five' | 'longer' | 'L' | 'T' | 'cross' | 'square' | 'group'

The hard-coded shape registry. longer = straight run of 6+.

MoveResult

interface MoveResult

What one move fully resolved to. All logic already happened; play steps back.

  • valid: boolean — false → nothing changed; steps holds the swap/swapBack for the fail fx.
  • steps: Step[] — The ordered event journal. Play the steps back one visual beat at a time.
  • region: number[] — Every cell index (y * cols + x) touched — for parallel-move admission.
  • totals: { matches: number; blocksCleared: number; chainDepth: number; specialsMade: number; specialsFired: number; score: number; /** Cleared blocks per colour — the resource layer (mana, orders) reads this. */ clearedByColor: Record<number, number>; /** Cleared blocks per tag (skulls, crates, gems…). */ clearedByTag: Record<string, number>; } — Aggregate counters for the whole move: matches, clears, cascade depth, specials, score.
  • goalsDelta: GoalDelta[] — One entry for each goal this move progressed.
  • gameOver?: 'bomb' | 'noMoves' — A lose trigger fired during this move (bomb expiry / unfixable deadlock).

OverlayEntry

interface OverlayEntry

An overlay entry sitting on top of one cell's block.

  • kind: string — Game-defined label, e.g. 'chain', 'vine', 'cage'.
  • hp: number — Hits remaining before the overlay clears.
  • locksMove?: boolean — Block cannot be swapped/moved (it can still fall away unless this is set).
  • locksMatch?: boolean — Block cannot take part in a match at all (cage/bubble style).
  • absorbsClear?: boolean — true (ice-casing style): a match ON the block damages the overlay and the block SURVIVES. false (chain style): the match clears overlay and block together.
  • hitFilter?: HitType[] — Which hit types damage it. Default: all.
  • spreads?: boolean — Vine/chocolate growth: each turn tick in which NO overlay of this kind was damaged, grow one copy onto a random adjacent uncovered block.

PredictedMove

interface PredictedMove

A legal move found by findMoves, with its predicted primary outcome.

  • kind: 'swap' | 'tap' | 'path' — The move type: a swap of two cells, a group tap, or a drawn path.
  • a: XY — The first cell of the move (the tapped cell, or one end of the swap).
  • b?: XY — Present for swaps.
  • path?: XY[] — Present for chain-preset moves: a playable path through one colour group.
  • shape: MatchShape — Predicted best shape the move makes (or group for taps).
  • length: number — Blocks in the predicted match/group.
  • value: number — Heuristic value — findMoves sorts by this, descending.

RefillSpawn

interface RefillSpawn

A refilled block. Animate it EXACTLY like a FallMove: walk path at the same cells-per-second speed as every other item in the step. path starts ABOVE the board (negative y for top entries), already stacked by spawn order, so simultaneous spawns keep one-cell spacing and never overlap the blocks falling below them. entry/to/drop/stack are informational extras.

  • block: BlockSnapshot — The new block, snapshotted at spawn time.
  • column: number — The column the block enters through.
  • entry: XY — The spawner cell the block enters through.
  • to: XY — The cell the block comes to rest in.
  • drop: number — The number of cells the block falls, for timing.
  • stack: number — The block's position in this column's spawn order (0 = first).
  • path: XY[] — Every cell the block traverses, starting above the board. Tween through it.

Run

interface Run

One straight run of three or more same-color cells found by the scanner.

  • cells: XY[] — The cells in the run, in scan order.
  • orientation: 'h' | 'v' — The direction of the run: 'h' for horizontal, 'v' for vertical.

SpawnWeights

type SpawnWeights = number[]

Weighted content table for refill spawns. Index = colour, value = weight.

SpecialDef

interface SpecialDef

The special payload carried by a special block.

  • kind: SpecialKind — Which built-in effect fires when the special activates.
  • radius?: number — Blast radius in cells for blast (1 = 3×3). Default 1.
  • action?: Action — Explicit action for kind: 'custom'.

SpecialKind

type SpecialKind = 'rowClear' | 'colClear' | 'blast' | 'colorClear' | 'seeker' | 'custom'

Built-in special payload kinds. custom fires action.

Step

type Step = | { type: 'swap'; a: XY; b: XY; aBlock: number; bBlock: number } /** The failed-swap bounce back. A `valid: false` result is exactly [swap, swapBack]. */ | { type: 'swapBack'; a: XY; b: XY; aBlock: number; bBlock: number } /** Collapse preset: the tapped flood group about to clear. */ | { type: 'tap'; at: XY; group: XY[] } /** Marks each cascade round, for escalating pitch/fx. depth 0 = the direct result. */ | { type: 'cascadeStart'; depth: number } | { type: 'match'; matches: MatchEvent[] } /** `fromShape: 'convert'` = mass conversion by a colour-clear combo. */ | { type: 'specialSpawn'; block: BlockSnapshot; at: XY; fromShape: MatchShape | 'convert' } /** A special (or pair combo) firing. `cells` = every cell it hits. */ | { type: 'detonate'; sourceId: number | null; at: XY; kind: SpecialKind | 'combo'; cells: XY[] } | { type: 'overlayDamage'; hits: { at: XY; kind: string; remainingHp: number }[] } | { type: 'cellLayerDamage'; hits: { at: XY; kind: string; remainingHp: number }[] } /** Multi-hp blocks cracking (ice), or blockers losing a layer. */ | { type: 'blockDamage'; hits: { blockId: number; at: XY; remainingHp: number }[] } | { type: 'clear'; blocks: { id: number; at: XY; block: BlockSnapshot; cause: HitType }[] } | { type: 'paint'; blockId: number; at: XY; color: number } | { type: 'fall'; moves: FallMove[]; spawns: RefillSpawn[] } | { type: 'conveyor'; shifts: FallMove[] } | { type: 'spread'; growth: { kind: string; at: XY }[] } | { type: 'bombTick'; ticks: { blockId: number; at: XY; remaining: number }[] } | { type: 'ingredientExit'; blockId: number; at: XY; block: BlockSnapshot } /** Move operators (A1). `rotate` = a 2×2 turn; `shift` = a wraparound line slide. */ | { type: 'rotate'; at: XY; dir: 'cw' | 'ccw'; blocks: number[] } | { type: 'shift'; axis: 'row' | 'col'; index: number; by: number; moves: FallMove[] } /** Chain preset: the committed player path. `loop` = the path closed a cycle. */ | { type: 'path'; cells: XY[]; loop: boolean } /** Merge mode: the consumed blocks (`from`) became one tier-`tier` block at `at`. * Present it by flying the `from` sprites into `at` (their `clear` step precedes this). */ | { type: 'merge'; at: XY; blockId: number; block: BlockSnapshot; tier: number; color: number; from: { blockId: number; at: XY }[] } /** Merge mode: blocks levelled up by a neighbouring explosion. */ | { type: 'tierUp'; hits: { blockId: number; at: XY; tier: number }[] } /** Merge mode: a max-tier block detonating (its `clear` follows). */ | { type: 'explode'; at: XY; blockId: number } /** Deadlock auto-reshuffle: every moved block with from → to. */ | { type: 'shuffle'; moves: { blockId: number; from: XY; to: XY }[] } | { type: 'gameOver'; reason: 'bomb' | 'noMoves' }

One journal step = one visual beat. Steps are strictly sequential; events inside a step are simultaneous (tween them together).

XY

interface XY

A board coordinate. x is the column (0 = left), y is the row (0 = top).

  • x: number — The column, 0 = left.
  • y: number — The row, 0 = top.