Phaser AE

Container sorting puzzles

Pro packv3.0.0-3Grid

The rules of the tube-sorting puzzles: pour the top colour from tube to tube until every tube holds one colour. Pouring liquids and moving balls are two settings of the same rules, and every generated deal is proven solvable.

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

Container sorting 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

containerSort

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

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

Helpers

createContainerSort

function createContainerSort(config: SortConfig, certificate?: SortAction[]): ContainerSort

Create a sorting puzzle. Throws on 1 or more than 24 containers, a capacity outside 1 to 32 (or not one per container), an overfull container, an empty colour key, a bad maxMoves or transfer, or a certificate that does not replay to a sorted position.

generateContainerSort

function generateContainerSort(options: SortGenerationOptions = {}): SortGeneration

Deal a seeded puzzle with a proven solution. By default (fullStart) it shuffles every colour into full containers plus empty spares and keeps the first of attempts candidates that solveGuided solves and that meets minFragmentation (else the most fragmented one solved). Throws when no candidate is solved within searchBudget, and on out-of-range options.

Classes

ContainerSort

class ContainerSort

Capacity-limited run transfers, with undo and bounded hints.

  • constructor(config: SortConfig, private certificate?: SortAction[])
  • get state(): SortState — A copy of the current position; changing it does not affect the puzzle.
  • capacity(index: number): number — Capacity of container index; 0 when there is no such container.
  • get solved(): boolean — Whether the position is sorted: every non-empty container holds one colour and is full, or, with requireFull: false, holds one colour with at most one part-filled container per colour.
  • get lost(): boolean — Whether the puzzle is unsolved and the move limit is reached or no legal move is left.
  • legalActions(): SortAction[] — Every legal move from the current position.
  • preview(a: SortAction): ActionResult<SortEvent> — The outcome of move a without playing it; changes nothing.
  • apply(a: SortAction): ActionResult<SortEvent> — Play move a. An illegal move changes nothing and returns valid: false with a reason: invalid-container, move-limit, empty-source, full-destination or different-color.
  • solve(maxStates = 20000): SearchResult<SortAction> — Exact breadth-first search from the current position, visiting at most maxStates positions (default 20000); changes nothing.
  • solveGuided(maxStates=20000):SearchResult<SortAction> — Bounded depth-first continuation search with equivalent empty tubes collapsed. No optimality claim.
  • solveBest(maxStates=20000):SearchResult<SortAction> — Revalidate the construction route from the live position before returning it.
  • undo(): boolean — Take back the last move; false when there is none.
  • redo(): boolean — Replay the move last taken back; false when there is none.
  • reset(): void — Return to the dealt position and clear the undo and redo history.

Types

ContainerSortApi

type ContainerSortApi = typeof kit

Public headless container sorting puzzles helpers.

SortAction

interface SortAction

A move: pour from one container into another.

  • from: number — Index of the container poured from.
  • to: number — Index of the container poured into; must differ from from.

SortConfig

interface SortConfig

Contents are bottom-first. Capacity may differ between containers.

  • containers: string[][] — The colour keys in each container, bottom first: 2 to 24 containers, none holding more than its capacity, every key a non-empty string.
  • capacity: number | number[] — Cells in every container (one number), or one capacity per container; each 1 to 32.
  • requireFull?: boolean — True (the default): sorted means every non-empty container is full of one colour. False: containers may be part full, but a colour may leave only one of its containers part full, so it is gathered as tightly as the containers allow (three reds in tubes of four sit in one tube, five fill one and part of another).
  • maxMoves?: number — Most moves allowed, at least 1; the puzzle is lost when it is reached unsolved. Default none.
  • transfer?: 'run'|'single' — run pours the whole matching top run that fits; single moves one top cell. Default run.

SortEvent

interface SortEvent

What a move did.

  • type: 'transferred' — The kind of event; always transferred.
  • from: number — Index of the container poured from.
  • to: number — Index of the container poured into.
  • color: string — The colour key moved.
  • count: number — How many cells moved.

SortGeneration

interface SortGeneration

A generated deal: the puzzle, a winning solution and how mixed it is.

  • config: SortConfig — The puzzle, ready for createContainerSort.
  • solution: SortAction[] — A replay-checked winning sequence of moves; pass it to createContainerSort so solveBest can use it.
  • scrambled: number — Reverse-scramble moves used (fullStart: false); 0 for a full-start deal.
  • seed: number — The seed asked for.
  • metrics: { /** Runs of one colour, counted over every container. */ runs: number; /** Containers holding more than one colour. */ mixed: number; /** \runs` minus the number of colours: 0 when sorted. / fragmentation: number; /* Fewest moves any solution needs, from fragmentation: a move merges at most one pair of runs. / lowerBound: number; /* Whether `fragmentation` reached `minFragmentation`. */ targetMet: boolean; }` — How mixed the deal is; none of it is a shortest-solution claim.

SortGenerationOptions

interface SortGenerationOptions

What generateContainerSort takes; every field may be left out.

  • colors?: string[] — The distinct colour keys to deal, at least 2. Default 3, 5, 7 or 9 numbered keys by difficulty.
  • capacity?: number — Cells in every container, 2 to 12. Default 4 (5 on very-hard).
  • empty?: number — Spare empty containers, 1 to 6. Default 2.
  • steps?: number — Reverse-scramble moves, 1 to 200; used only when fullStart is false. Default 24, 48, 88 or 120 by difficulty.
  • seed?: number — Unsigned 32-bit seed; the same seed and options give the same deal. Default 1.
  • transfer?: 'run'|'single' — Transfer rule the deal is proved under. Default run.
  • difficulty?: 'easy'|'medium'|'hard'|'very-hard' — Tier that sets the defaults for colours, capacity, steps and minFragmentation. Default medium.
  • minFragmentation?: number — Fragmentation (colour runs minus colours) to aim for, 0 to 200; attempts continue until it is met, keeping the best. Default 3, 8, 14 or 20 by difficulty.
  • attempts?: number — Candidate deals to try, 1 to 64. Default 12.
  • fullStart?: boolean — Deal full mixed containers plus spares and prove them solvable (default true); false builds by reverse scrambling, which can leave containers part filled.
  • searchBudget?: number — Positions the solver may visit per candidate when proving it, 1 to 1,000,000. Default 10,000.

SortState

interface SortState

A position: what each container holds and how many moves have been played.

  • containers: string[][] — The colour keys in each container, bottom first.
  • moves: number — Moves played from the dealt position.