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), otherwisesolve(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. Offershuffle,undoorreset. A table with legal pairs can still be lost; onlysolvecan 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>— Whatapplywould 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 withinmaxStatespositions, default 20,000), so the table is solvable afterwards. One move in the history, soundotakes it back. Invalid with the reasonsolved,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) orbudget-exhausted. Throws on a bad seed or budget.solve(maxStates = 20000): SearchResult<PairAction>— A winning continuation from here by depth-first search over at mostmaxStatespositions (default 20,000), without changing anything.unsolvableis a proof;budget-exhaustedis 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 asa.
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:tilecannot be picked whilebyis 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 forcreateTilePairs.solution: PairAction[]— A replay-checked order that clears the table; pass it tocreateTilePairssosolveBestcan 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 frommahjongTurtle(which ignorescols,rowsandlayers, 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 fromdifficulty.rows?: number— Tiles down the bottom layer (the grid's rows in Onet): 2 to 10. Default fromdifficulty.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 fromdifficulty.groups?: number— Distinct matching groups the tiles are dealt into, 2 to 36 (mahjong's 34 faces plus flowers and seasons). Default fromdifficulty, 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.


