Phaser AE

Layered tile matching

Pro packv3.0.0-3Grid

The rules of pair-matching tile games: layered mahjong solitaire and flat connect-the-pair boards. Every deal can be cleared, a dead end can be reshuffled into a table that still can, and hints, undo and a solver come with it.

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

Layered tile matching: 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

tilePairs

function tilePairs(): Pack<{ tilePairs: typeof kit }>

Install pure rules; no renderer, DOM or simulation hook is required.

Helpers

createTilePairs

function createTilePairs(config: PairsConfig, certificate?: PairAction[]): TilePairs

Create a mahjong or Onet game. Throws on a deal it refuses (see PairsConfig) or a certificate that does not replay to an empty table.

dealTilePairs

function dealTilePairs(config: Omit<PairsConfig, 'tiles'> & { tiles: Omit<PairTile, 'key'>[] }, keys: string[], seed = 1, maxStates = 20000, exact = false): { status: 'solved'; config: PairsConfig; solution: PairAction[] } | { status: 'unsolvable' | 'budget-exhausted' }

Deal keys onto an authored layout so it can be cleared: find an order in which the tiles can be removed two at a time (depth-first, within maxStates positions, default 20,000), then give each removed pair one of keys, cycling through them, shuffled by seed. With exact, keys lists one key per pair and each is used once. Returns the keyed config and that removal order as a solution, or unsolvable (no order exists, whatever the keys) or budget-exhausted (no claim). Throws on empty keys, a bad seed, or exact keys that are not one per pair.

generateTilePairs

function generateTilePairs(options: PairsGenerationOptions = {}): PairsGeneration

Build a layout and deal keys onto it along a proven removal order, then replay it. Throws on out-of-range options, a layout over 200 tiles, or a deal whose search fails (generation: unsolvable or generation: budget-exhausted).

mahjongTurtle

function mahjongTurtle(): Omit<PairTile, 'key'>[]

The classic Turtle (Dragon) layout of mahjong solitaire: 144 tile footprints on five layers, a 12 x 8 base with its notched rows and three wing tiles, then 6 x 6, 4 x 4 and 2 x 2 layers and a single tile on top straddling four. Positions are in tile widths and heights, as PairTile takes them. Deal keys onto it with dealTilePairs (36 keys of four tiles is the real set), or ask generateTilePairs for layout: 'turtle'.

Classes

TilePairs

class TilePairs

Mahjong solitaire and Onet rules, independent of the renderer and camera. Get one from createTilePairs.

  • constructor(config: PairsConfig, private certificate?: PairAction[])
  • solveBest(maxStates = 20000): SearchResult<PairAction> — A winning continuation, without changing anything: the rest of the deal's certificate when it still works from here (until a shuffle), otherwise solve(maxStates). Its first pair is the hint.
  • get tiles(): PairTile[] — A copy of every tile in the deal, removed or not, with its current key (a shuffle changes keys).
  • get state(): PairsState — A copy of the current state.
  • get solved(): boolean — Whether every tile has been removed.
  • get stuck(): boolean — Whether the game is at a dead end: tiles remain but no pair can be removed. Offer shuffle, undo or reset. A table with legal pairs can still be lost; only solve can tell.
  • blockedBy(id: string): string[] — Why a tile cannot be taken now: 'covered-by:<id>', 'both-sides-blocked', 'blocked-by:<id>' or 'removed-or-unknown'; empty when it is free.
  • isFree(id: string): boolean — Whether the tile is on the table and nothing blocks it.
  • legalActions(): PairAction[] — Every pair that could be removed now: free tiles with equal keys (and, in Onet, a link between them).
  • preview(a: PairAction): ActionResult<PairEvent> — What apply would return for this pair, without playing it or touching the state or history.
  • apply(a: PairAction): ActionResult<PairEvent> — Remove a pair if it is legal. Returns { valid: true, events }, or { valid: false, reason, events: [] } with a reason such as 'invalid-pair', 'different-group', 'blocked-tile' or 'no-path'. A move discards any undone moves.
  • shuffle(seed = 1, maxStates = 20000): ActionResult<PairShuffleEvent> — Reshuffle the faces on the table, as mahjong games do at a dead end: the remaining tiles keep their places, and their keys are dealt again along a removal order proven to clear them (found within maxStates positions, default 20,000), so the table is solvable afterwards. One move in the history, so undo takes it back. Invalid with the reason solved, unpaired-keys (a key left an odd number of times), unsolvable (no order clears the table whatever the keys, which a stack of tiles can cause) or budget-exhausted. Throws on a bad seed or budget.
  • solve(maxStates = 20000): SearchResult<PairAction> — A winning continuation from here by depth-first search over at most maxStates positions (default 20,000), without changing anything. unsolvable is a proof; budget-exhausted is not.
  • undo(): boolean — Take back the last pair or shuffle; false when there is nothing to undo.
  • redo(): boolean — Play again a pair or shuffle that was taken back; false when there is nothing to redo.
  • reset(): void — Return to the full deal and clear both the undo and redo histories.

Types

PairAction

interface PairAction

Removing two tiles together.

  • a: string — The id of the first tile.
  • b: string — The id of the second tile; not the same tile as a.

PairEvent

interface PairEvent

What a successful move reports.

  • type: 'removed' — Always 'removed': a pair left the table.
  • ids: [string, string] — The ids of the two tiles removed.
  • path: Cell[] — The cells the link runs through, from the first tile's cell to the second's, possibly via the outside border; empty in mahjong.

PairsConfig

interface PairsConfig

A deal: the rules to play by and the tiles to play on. The constructor throws on a deal it refuses.

  • mode: 'mahjong' | 'onet' — 'mahjong' frees a tile by footprint (nothing above it and an open side); 'onet' joins two tiles by a path with few bends.
  • tiles: PairTile[] — The tiles: an even number from 2 to 200, with no two overlapping on one layer.
  • cols?: number — Onet only: columns in the grid, 1 to 32; required.
  • rows?: number — Onet only: rows in the grid, 1 to 32; required.
  • maxTurns?: number — Onet only: the most bends a joining path may have, 0 to 2. Default 2.
  • blockers?: { tile: string; by: string }[] — Extra blocking: tile cannot be picked while by is still on the table. Both are tile ids, and not the same tile.

PairsGeneration

interface PairsGeneration

A generated deal: the game, a winning solution and what was measured.

  • config: PairsConfig — The deal, ready for createTilePairs.
  • solution: PairAction[] — A replay-checked order that clears the table; pass it to createTilePairs so solveBest can use it.
  • seed: number — The seed asked for.
  • metrics: { /** Tiles dealt. */ tiles: number; /** Layers used. */ layers: number; /** Tiles free at the start. */ exposed: number; /** Pairs removable at the start. */ legalPairs: number; /** Distinct keys dealt. */ groups: number; } — The deal's structure, not a human difficulty or a uniqueness claim.

PairsGenerationOptions

interface PairsGenerationOptions

What generateTilePairs takes; every field may be left out.

  • mode?: PairsConfig['mode'] — 'mahjong' or 'onet'. Default 'mahjong'.
  • seed?: number — Unsigned 32-bit integer; the same options and seed make the same deal. Default 1.
  • difficulty?: 'easy'|'medium'|'hard'|'very-hard' — Sets the defaults for columns, rows, layers and groups (6/8/10/12 columns, 4/6/6/8 rows, 2/3/3/4 layers, 8/12/18/24 groups from easy to very hard). Default 'medium'.
  • layout?: 'stepped'|'turtle'|'diamond'|'bridge'|'pyramid'|'rectangle' — The shape of the layers: 'stepped' (the default in mahjong: rectangles whose first and last rows are two shorter), 'diamond', 'bridge', 'pyramid', 'rectangle' (Onet's only, and default), or 'turtle', the classic 144-tile Turtle from mahjongTurtle (which ignores cols, rows and layers, and defaults to 36 groups).
  • cols?: number — Tiles across the bottom layer (the grid's columns in Onet): an even number from 4 to 14, so every row pairs up and the 200-tile cap holds. Default from difficulty.
  • rows?: number — Tiles down the bottom layer (the grid's rows in Onet): 2 to 10. Default from difficulty.
  • layers?: number — Layers stacked in mahjong, 1 to 4 (each is two tiles narrower and shorter than the one below); Onet is always 1. Default from difficulty.
  • groups?: number — Distinct matching groups the tiles are dealt into, 2 to 36 (mahjong's 34 faces plus flowers and seasons). Default from difficulty, or 36 for the turtle.
  • maxStates?: number — The most states the deal's solving search may visit, 1 to 1,000,000. Default 20000.

PairShuffleEvent

interface PairShuffleEvent

What a successful shuffle reports: the new key of every tile still on the table.

  • type: 'shuffled' — Always 'shuffled'.
  • keys: Record<string, string> — New matching key by tile id, for the tiles still on the table.

PairsState

interface PairsState

How a game stands.

  • removed: string[] — Ids of the tiles taken off the table, two to a move, in the order they went.
  • moves: number — Pairs removed so far.
  • shuffles: number — Shuffles played so far.
  • keys: Record<string, string> — Each tile's matching key now, by id: the dealt key until a shuffle changes it.

PairTile

interface PairTile

A tile: where it lies and which matching group it belongs to. Distinct faces can share a key, as mahjong's flowers do.

  • id: string — Unique among the deal's tiles; what actions and events name.
  • key: string — The matching group: two tiles can be removed together only when their keys are equal. Not empty.
  • x: number — Left edge of the footprint, in tile widths; in Onet the cell column, a whole number from 0. Mahjong may use half-tile offsets.
  • y: number — Top edge of the footprint, in tile heights; in Onet the cell row, a whole number from 0.
  • z?: number — Layer, a whole number from 0 to 32; a higher layer covers the tiles under it. Default 0; Onet is all layer 0.
  • width?: number — Footprint width in tile widths, above 0. Default 1; Onet tiles are 1.
  • height?: number — Footprint height in tile heights, above 0. Default 1; Onet tiles are 1.

TilePairsApi

type TilePairsApi = typeof kit

Public headless layered tile matching helpers.