Phaser AE

Merge workshop puzzles

Pro packv3.0.0-3Grid

Merge puzzles with a plan to them: pairs of identical items climb their chains to fill exact orders, supplies arrive from finite queues and space runs out. Every generated level comes with a replayed winning route, and harder ones are proven to defeat simple play.

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

Released October 14, 2026 · updated October 19, 2026

Merge workshop puzzles: 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

mergeWorkshop

function mergeWorkshop(): Pack<{ mergeWorkshop: MergeWorkshopApi; }>

Install renderer-independent merge rules and cancellable worker jobs.

Helpers

createMergeWorkshop

function createMergeWorkshop(config: MergeConfig)

Builds a merge workshop from a level config. Throws if the config is invalid.

generateMergeWorkshop

function generateMergeWorkshop(options: MergeGenerateOptions = {}): MergeLevel

Generate the actual order forest and finite resources, then prove a route through the public rules.

playMergeStrategy

function playMergeStrategy(config:MergeConfig,strategy:MergeStrategy,maxStates=2000):MergeStrategyResult

A deterministic player with no lookahead, search or access to the solution certificate. All three reserve exact order items; they differ in which supply they select. Terminal space failures are proved separately, never inferred from a search timeout.

Classes

MergeWorkshop

class MergeWorkshop

The rules of a merge-and-deliver puzzle: merge matching items up a tier, fill orders from the board and storage, and draw new items from producers with energy and cooldowns. Moves are undoable. Build one with createMergeWorkshop; it throws if the config is invalid.

  • readonly config: MergeConfig — The level as given (a copy).
  • constructor(config: MergeConfig)
  • get state() — A copy of the current state.
  • get solved() — Whether every order has been fulfilled.
  • isOpen(index: number, s = this.history.state) — Whether cell number index exists and is open: enough orders are complete for its unlockAfter. Checks the current state unless one is given.
  • transition(state: MergeState, action: MergeAction): { valid: true; state: MergeState; } | { valid: false; reason: string; } — Pure atomic transition; rejection never consumes inventory, energy or history.
  • inspect(a: MergeAction) — Reports whether an action would be accepted now, as { valid, reason }, without playing it. reason is empty when valid.
  • apply(a: MergeAction) — Plays an action and records it for undo. Returns { valid: true, reason: '' }, or { valid: false, reason } with the state untouched.
  • undo() — Takes back the last action. Returns false when there is nothing to undo.
  • redo() — Replays the action just undone. Returns false when there is nothing to redo.
  • reset() — Returns to the opening position and forgets the actions played.
  • restore(state: MergeState) — Make state the new opening position, with no moves behind it: reset returns here. A state that fails validation changes nothing.
  • resourceFailure(s = this.history.state): string | null — Necessary resource proof: reserve lower exact orders before carrying pairs upward.
  • requiredMerges(s = this.history.state): number | null — Exact for a mass-balanced finite supply: every item is required and every merge reduces item count by one.
  • actions(s = this.history.state): MergeAction[] — Symmetry reduction is exact: free cells have unrestricted drag access; sealed items are distinct by type.
  • solve(maxStates = 20000): MergeSolution — Searches for a route that completes every order from the current state, visiting at most maxStates distinct states (a whole number from 1 to 1,000,000; default 20000) and routes up to 512 actions long. Throws if maxStates is out of range. It does not change the game.
  • get lost() — Cheap proof only. For full deadlock detection call solve(), which distinguishes budget exhaustion.

Constants

mergeLayouts

const mergeLayouts: readonly ["rectangle", "courtyard", "wings", "terraces", "islands", "mosaic"]

The board shapes the generator can lay out, as names for MergeGenerateOptions.layout.

mergeStrategies

const mergeStrategies: readonly ["queue-first", "match-first", "order-first"]

The built-in no-lookahead play styles playMergeStrategy can follow: queue-first draws from producers first, match-first prefers items that match one already on the board, order-first prefers items an available order needs.

Types

MergeAction

type MergeAction = { type: 'merge' | 'move'; from: number; to: number; } | { type: 'produce'; producer: number; to: number; } | { type: 'fulfil'; order: string; } | { type: 'store'; from: number; } | { type: 'retrieve'; from: number; to: number; } | { type: 'wait'; ticks: number; }

One thing the player can do. Every action is atomic: it either happens in full or is rejected and changes nothing.

  • merge: combine the item in cell from into the matching item in cell to, which goes up a tier.
  • move: move the item in cell from to the empty cell to.
  • produce: take the next item from producer number producer (an index into producers) into the empty cell to.
  • fulfil: deliver the order with this id.
  • store: put the item in cell from into storage.
  • retrieve: bring the item at storage index from out into the empty cell to.
  • wait: let ticks pass (1 to 10000), for producers to cool down.

MergeCell

interface MergeCell

One place on the board where an item can sit.

  • id: string — Unique name for the cell.
  • x: number — Column on the board's grid, from 0 to 19.
  • y: number — Row on the board's grid, from 0 to 19.
  • unlockAfter?: number — Number of orders that must be complete before the cell opens; default 0, open from the start. A cell that is closed at the start holds no item.

MergeConfig

interface MergeConfig

A complete level: the chains, the board, the starting items, the orders, the producers and the energy and storage allowances. new MergeWorkshop throws on a config it cannot play.

  • id: string — Name of the level; not empty.
  • chains: { id: string; maxTier: number; }[] — The item chains, 1 to 8, each with a unique id and the highest tier it reaches (1 to 10).
  • cells: MergeCell[] — The board's cells, 2 to 100 with unique ids and unique positions. Items are addressed by index into this list.
  • items: (MergeItem | null)[] — The item starting in each cell, in the order of cells, or null for an empty cell: exactly one entry per cell.
  • orders: MergeOrder[] — The orders to fulfil, 1 to 24. The level is complete when all are fulfilled.
  • producers: MergeProducer[] — The producers, 0 to 8.
  • storage: number — Storage slots for items taken off the board, 0 to 16.
  • energy: number — Energy at the start, 0 to 1,000,000; each production spends its producer's energyCost.

MergeGenerateOptions

interface MergeGenerateOptions

Controls for generateMergeWorkshop. Each defaults from difficulty, so a bare call is a complete level; set one to change that part of the puzzle and keep the rest.

  • seed?: number — Seed for the whole level, a whole number from 0 to 4294967295; default 1. The same seed and options give the same level.
  • difficulty?: 'easy' | 'medium' | 'hard' | 'very-hard' — Sets the defaults for every other control: easy, medium, hard or very-hard; default medium. Harder levels have more chains, orders and merges, less working space and plan-ahead supply queues.
  • layout?: MergeLayout | 'auto' — The board shape: one of mergeLayouts, or auto (default), which picks one by seed. Throws on any other name.
  • chains?: number — Item chains in play, 1 to 6. Default 2 to 5 by difficulty (easy 2 or 3).
  • orders?: number — Orders to fill, 1 to 12. Default 2 to 9 by difficulty.
  • merges?: number — Merges the solution needs, exactly, 1 to 80. Default about 3, 11, 23 or 39 by difficulty, plus a little variation.
  • workspace?: number — Empty open cells at the start, 1 to 12; fewer is harder. Default 4, 3, 2 or 1 by difficulty (easy adds up to 2).
  • storage?: number — Storage slots, 0 to 8. Default 3 on easy, otherwise 1.
  • sealed?: number — Starting items to seal, 0 to 12; default 0. A sealed item cannot be moved, stored or delivered until a matching item is merged onto it. The generator seals fewer if no matching pair is available.
  • producers?: number — Producers with supply queues, 0 to 6. Default 0 on easy, otherwise 3. With none, every item starts on the board.
  • challenge?: boolean — Whether to accept only levels where each of the mergeStrategies ends in a space dead end, so the player has to plan. Default true except on easy.

MergeItem

interface MergeItem

One piece on the board, in storage or in a producer's queue: an item of a chain at a tier.

  • chain: string — Id of the chain it belongs to, from MergeConfig.chains.
  • tier: number — Its level in the chain, a whole number from 0 to the chain's maxTier; two items of the same chain and tier merge into one a tier higher.
  • sealed?: boolean — Whether it is fixed: it cannot be moved, stored or delivered, but a matching item can still be merged onto it, which frees the result. Default false.

MergeLevel

interface MergeLevel

A generated level: a playable config, a solution proved by the rules, and what was asked for and measured.

  • seed: number — The seed it was generated from.
  • difficulty: NonNullable<MergeGenerateOptions['difficulty']> — The difficulty it was generated for.
  • layout: MergeLayout — The board shape used, resolved from auto if that was asked for.
  • config: MergeConfig — The playable level, for createMergeWorkshop.
  • solution: MergeAction[] — A sequence of actions that completes every order when played from the start, found and replayed by the generator.
  • parameters: Required<MergeGenerateOptions> — Every control as used, defaults filled in; layout is as requested, so it may still be auto.
  • metrics: { requiredMerges: number; solutionActions: number; chains: number; orders: number; sealed: number; supplies: number; initialFree: number; unlockCells: number; searchStates: number; structure: string; challenge: boolean; strategies: { strategy: MergeStrategyResult['strategy']; outcome: MergeStrategyResult['outcome']; }[]; } — Figures measured on the level: requiredMerges and solutionActions of the solution, the counts of chains, orders, sealed items, supplies held in producer queues, initialFree cells and unlockCells locked at the start, searchStates the solver visited, a layout-free structure signature for comparing levels, whether challenge was applied, and the outcome of each strategies play.

MergeOrder

interface MergeOrder

Something to deliver: a set of items that are used up when it is fulfilled.

  • id: string — Unique name for the order.
  • items: { chain: string; tier: number; count: number; }[] — The items it consumes: for each entry, count items of the chain at exactly that tier, taken from the board first and then from storage. Sealed items do not count. 1 to 16 entries, each count 1 to 64.
  • requires?: string[] — Ids of orders that must be complete before this one can be fulfilled; the dependencies may not form a cycle.
  • reward?: number — Credits added to MergeState.credits when it is fulfilled, a whole number from 0 to 1,000,000; default 0.

MergeProducer

interface MergeProducer

A source of items that hands them out one at a time, in order, for energy.

  • id: string — Unique name for the producer.
  • queue: MergeItem[] — The items it will produce, in order: up to 128, none sealed.
  • requires?: string[] — Ids of orders that must be complete before it can produce.
  • cooldown?: number — Ticks that must pass after one production before it can produce again; a whole number from 0 to 1000, default 0.
  • energyCost?: number — Energy each production costs, a whole number from 0 to 100; default 1.

MergeSolution

interface MergeSolution

The result of solve.

  • status: 'solved' | 'unsolvable' | 'budget-exhausted' — solved if a route was found, unsolvable if the search proved there is none, or budget-exhausted if it hit the state or depth limit first.
  • actions: MergeAction[] — The actions of the route when solved; empty otherwise.
  • visited: number — How many distinct states the search visited.
  • reason: string — Why: validated-route when solved, search-limit when the budget ran out, otherwise the failing resource such as insufficient-gem-tier-2 or no-reachable-solution.
  • requiredMerges: number | null — The exact number of merges any solution needs, or null when the supply does not exactly match the orders.

MergeState

interface MergeState

Everything that changes during play. Plain data; save it and give it back to restore.

  • items: (MergeItem | null)[] — The item in each cell, in the order of MergeConfig.cells, or null when empty.
  • storage: MergeItem[] — The items in storage, oldest first.
  • produced: number[] — For each producer, how many items of its queue it has already produced.
  • ready: number[] — For each producer, the tick from which it can next produce.
  • completed: string[] — Ids of the orders fulfilled so far, in the order they were completed.
  • energy: number — Energy left.
  • credits: number — Credits earned from order rewards so far.
  • tick: number — Time in ticks: each accepted action adds one and wait adds several. Cooldowns count against it.
  • merges: number — Merges made so far.
  • productions: number — Items produced so far.
  • deliveries: number — Orders fulfilled so far.
  • rearrangements: number — Moves, stores and retrievals so far.

MergeStrategy

type MergeStrategy=typeof mergeStrategies[number]

One of mergeStrategies.

MergeStrategyResult

interface MergeStrategyResult

How one strategy fared on a level.

  • strategy:MergeStrategy — The strategy that was played.
  • outcome:'solved'|'resource-dead-end'|'space-dead-end'|'unresolved' — solved if it completed every order; resource-dead-end if the items left cannot fill the remaining orders; space-dead-end if the supply suffices but a complete search shows no way on from where it stopped; unresolved if neither could be shown.
  • actions:MergeAction[] — The actions it played, in order.
  • state:MergeState — The state where it stopped.

MergeWorkshopApi

type MergeWorkshopApi = typeof kit & { generateMergeWorkshopAsync(options?: kit.MergeGenerateOptions, job?: { signal?: AbortSignal; }): Promise<kit.MergeLevel>; solveMergeWorkshopAsync(config: kit.MergeConfig, state: kit.MergeState, maxStates?: number, job?: { signal?: AbortSignal; }): Promise<kit.MergeSolution>; }

Public independent rules and helpers exposed by the composed pack.