Phaser AE

Conveyor shooter sort

Pro packv3.0.0-0GridNew

The rules of the conveyor sort puzzle, with the mechanics found in games like Pixel Flow. You draw the picture, the belt and the shooters; the pack decides every shot, every bench slot and the win or the loss, and generates levels that come with their own proof.

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

Released October 24, 2026

Conveyor shooter sort: 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

conveyorSort

function conveyorSort(): Pack<{ conveyorSort: typeof kit; }>

Compose the conveyor sort rules, generators and solving player under game.conveyorSort.

Helpers

CONVEYOR_PALETTE

const CONVEYOR_PALETTE: readonly string[]

Twelve distinct cube colours, CSS hex strings.

conveyorHint

function conveyorHint(game: ConveyorSort): ConveyorHint | null

The next shooter worth sending, or null when none is (wait for the riders on the belt, or the level is stuck). Prefers a shooter that spends all its shots, then the most hits, then the bench (to free a slot); never one that could come back to a bench without room. When nothing can hit and the belt is empty it digs: it sends the front shooter whose column has the most useful shooter behind it. A heuristic, not a solver; pure, so the state is unchanged.

conveyorPicture

function conveyorPicture(options: ConveyorPictureOptions): ConveyorPicture

Generate a seeded picture to clear. Throws on a size, colour count or style it cannot make.

conveyorPoint

function conveyorPoint(track: ConveyorTrack, pos: number, margin = 1): ConveyorPoint

Where a shooter pos units along the belt is drawn, in board cell units, with the belt margin cells outside the board's edge (default 1, from 0 to 16). Along a side it is level with the station it is passing; round a corner it follows a quarter circle. pos wraps at the lap length.

conveyorTrack

function conveyorTrack(width: number, height: number, options: ConveyorTrackOptions = {}): ConveyorTrack

Lay a belt round a width × height board. Stations sit one per column along the top and bottom and one per row along the sides, each a unit apart, with cornerGap units round each corner. Throws on a size outside 1 to 64 or an unknown option.

createConveyorSort

function createConveyorSort(config: ConveyorSortConfig): ConveyorSort

Make a conveyor sort level from a config. Throws, naming the field, on a config it cannot play.

generateConveyorSort

function generateConveyorSort(options: ConveyorGenerateOptions): ConveyorLevel

Generate a level: split each colour's hits into shooters, find an order that clears the picture by sending one shooter at a time, deal that order into the columns, then move shooters within their columns until the solving player needs about as much of the bench as the difficulty asks. The result comes with the winning taps, checked by playing them. Throws when no winnable arrangement turns up in attempts tries (a bench of 0 with large shots, say).

pictureFromRows

function pictureFromRows(rows: readonly string[], palette: Readonly<Record<string, string>>): ConveyorPicture

A picture from rows of characters, top row first: each character names a colour in palette and . or a space is an empty cell. Rows may differ in length; the widest sets the width. Throws on a character the palette lacks or a size outside 1 to 64.

playConveyor

function playConveyor(config: ConveyorSortConfig, options: { maxLaunches?: number } = {}): ConveyorPlay

Play a level to the end with conveyorHint, sending one shooter (or link group) at a time and running the belt until it is empty before the next. Stops after maxLaunches (default 2000). A win proves the level winnable; a loss proves only that this simple player lost.

replayConveyor

function replayConveyor(config: ConveyorSortConfig, launches: string[]): ConveyorPlay

Replay recorded launches, each when the belt is empty, and report how the level ended.

stationLine

function stationLine(track: ConveyorTrack, station: ConveyorStation): number[]

The board cells a station fires along, outermost first, as indices into a row-major width × height array.

Classes

ConveyorSort

class ConveyorSort

A conveyor sort level in play: shooters from the fronts of the columns ride a belt round the picture and fire at cubes of their colour; ones with shots left wait on a bench, and a return to a full bench loses. Time moves only through advance or tick, in whole fixed steps, so the same launches at the same steps replay exactly at any frame rate. Get one from createConveyorSort.

  • constructor(config: ConveyorSortConfig)
  • get config(): ConveyorSortConfig — A copy of the config this level was made from.
  • get state(): ConveyorState — A copy of the state: everything a save needs.
  • get track(): ConveyorTrack — The belt's layout.
  • get status(): ConveyorState['status'] — 'playing', 'won' or 'lost'.
  • get time(): number — Seconds of play so far: whole steps run divided by the step rate.
  • get remaining(): number — Cubes still on the board.
  • get progress(): number — Fraction of the picture's hits landed, 0 to 1.
  • get freeTrays(): number — Trays free at the gate: the trays less the shooters on the belt or waiting to get on.
  • get freeBench(): number — Free bench slots.
  • get busy(): number — Shooters on the belt or waiting at the gate to get on.
  • riders(): { id: string; pos: number; color: string; ammo: number }[] — The shooters on the belt, first on first, with their colour and shots left: what a presenter draws each frame.
  • colorOf(id: string): string | undefined — A shooter's colour, or undefined for an unknown id.
  • ammoOf(id: string): number — A shooter's shots left, or 0 for an unknown id.
  • cellAt(x: number, y: number): { color: string; hits: number } | null — The colour of the cube on a cell, or null when the cell is empty or off the board.
  • pointOf(id: string, margin = 1): ConveyorPoint | null — Where a shooter on the belt is drawn, in board cell units with the belt margin cells out; null if it is not on the belt.
  • pointAt(pos: number, margin = 1): ConveyorPoint — Where the belt pos stations from the gate is drawn; see conveyorPoint.
  • available(): ConveyorChoice[] — Every shooter that can be tapped now (each column's front and each bench shooter), with whether it can go.
  • refusal(id: string, pick = false): ConveyorRefusal | null — Why launch(id) (or pick(id) when pick is true) would be refused now, or null if it would be accepted.
  • launch(id: string): ConveyorResult — Send a shooter: the front of a column (with its whole link group) or one from the bench. It takes a tray and gets on the belt as soon as there is room at the gate.
  • pick(id: string): ConveyorResult — Booster: send any queued shooter, wherever it stands in its column, with its link group.
  • addBench(count = 1): ConveyorResult — Booster: add bench slots (1 to 8, default 1).
  • addTrays(count = 1): ConveyorResult — Booster: add trays at the gate (1 to 8, default 1).
  • shuffle(): ConveyorResult — Booster: shuffle the queued shooters that have no link among the places they stand, from the level's seed. Column lengths and linked shooters stay where they are.
  • continueRun(slots = 1): ConveyorResult — After a loss: add bench slots (1 to 8, default 1), put the shooter held at the gate on the first one and play on. The continue a game sells.
  • advance(seconds: number): ConveyorEvent[] — Run the simulation seconds on (0 to 10 per call), in whole steps; time left over is kept for the next call. Returns what happened, in order.
  • tick(): ConveyorEvent[] — Run one fixed step: the next waiting shooter gets on if there is room, then every rider moves and fires.
  • lap(id: string): ConveyorLap | undefined — What one lap of shooter id alone would do on the board as it is now, ignoring the riders already on the belt. Pure; the state is unchanged. Undefined for an unknown id.
  • laps(ids: string[]): ConveyorLap[] — Laps of several shooters sent one after another, each alone and each on the board the last one left; what a linked group would do, near enough. Pure. Throws on an unknown id.
  • exposed(): Record<string, number> — Cubes a shooter of each colour could hit right now from a firing station, each cube counted once.
  • restore(state: ConveyorState): void — Put back a state state gave, of this level. Throws on one that does not fit the level.

Types

ConveyorCell

interface ConveyorCell

One cube of the picture.

  • x: number — Column, from 0 at the left.
  • y: number — Row, from 0 at the top.
  • color: string — Colour, a CSS colour string: only shooters of exactly this string hit it.
  • hits?: number — Hits it takes to clear, 1 to 999; default 1.

ConveyorChoice

interface ConveyorChoice

A shooter that could be sent now, from available().

  • id: string — The shooter tapped.
  • group: string[] — Every shooter that would go with it (its link group), itself included.
  • from: 'column' | 'bench' — Where it is: the front of a column or a bench slot.
  • index: number — The column or bench slot index.
  • ok: boolean — Whether launch would accept it now.
  • reason?: ConveyorRefusal — Why not, when ok is false.

ConveyorCorner

type ConveyorCorner = 'bottom-left' | 'bottom-right' | 'top-right' | 'top-left'

A corner of the board: where the gate can stand.

ConveyorDifficulty

type ConveyorDifficulty = 'easy' | 'medium' | 'hard' | 'expert'

How hard a generated level is, graded by how much of the bench the solving player needs.

ConveyorEvent

type ConveyorEvent = | { type: 'launched'; id: string; from: 'column' | 'bench' | 'pick'; index: number } | { type: 'entered'; id: string } | { type: 'shot'; id: string; x: number; y: number; station: number; hitsLeft: number; cleared: boolean; ammo: number } | { type: 'spent'; id: string; pos: number } | { type: 'returned'; id: string; slot: number } | { type: 'lost'; id: string; reason: 'bench-full' } | { type: 'continued'; id: string; slot: number } | { type: 'won' }

What happened, in order. Positions are board cells.

ConveyorGenerateOptions

interface ConveyorGenerateOptions

What to generate. Only seed is required.

  • seed: number — Seed, a whole number from 0 to 4294967295. The same options give the same level.
  • picture?: ConveyorPicture — The picture to clear; default one from conveyorPicture with the size, colours and style below.
  • width?: number — Generated picture width, 4 to 64; default 20.
  • height?: number — Generated picture height, 4 to 64; default 20.
  • colors?: number — Generated picture colours, 2 to 12; default 4.
  • style?: ConveyorPictureStyle — Generated picture style; default 'sprite'.
  • columns?: number — Columns of shooters, 1 to 12; default 4.
  • ammo?: number[] — The shot counts shooters are made with, each 1 to 999; default [10, 20, 30, 40]. A colour's last shooter takes what is left.
  • difficulty?: ConveyorDifficulty — Difficulty; default 'medium'.
  • links?: number — Chance, 0 to 1, that two shooters of one colour sent one after the other in the solution are linked; default 0.
  • rules?: ConveyorRuleOptions — The rules: trays, bench, speed, line of fire, burst, track and so on.
  • attempts?: number — Arrangements to try for the difficulty, 1 to 400; default 60.

ConveyorHint

interface ConveyorHint

A suggested launch, from conveyorHint.

  • id: string — The shooter to tap.
  • hits: number — Shots its lap (its link group's laps) would land on the board as it is now.
  • why: 'clears' | 'hits' | 'digs' — 'clears': it would spend every shot. 'hits': it would land some and come back. 'digs': nothing can land a shot now, so it goes round empty to bring the shooter behind it to the front.

ConveyorLap

interface ConveyorLap

What one lap of a shooter alone would do on the board as it is now, from lap().

  • hits: number — Shots it would land.
  • spent: boolean — Whether it would run out of shots and leave before the gate.
  • cells: { x: number; y: number }[] — The cells it would hit, in order (a tough cube can appear more than once).

ConveyorLevel

interface ConveyorLevel

A generated level and the proof it can be won.

  • config: ConveyorSortConfig — The level, ready for createConveyorSort.
  • solution: string[] — A winning sequence of taps, each made when the belt is empty; replayConveyor checks it.
  • benchPeak: number — Most of the bench the solution needs at once.
  • difficulty: ConveyorDifficulty — The difficulty asked for.

ConveyorLineOfFire

type ConveyorLineOfFire = 'first' | 'first-match'

How a shooter picks its target in the line in front of a station.

ConveyorPicture

interface ConveyorPicture

A picture of cubes: its size and its cubes.

  • width: number — Width in cells.
  • height: number — Height in cells.
  • cells: ConveyorCell[] — The cubes, one per filled cell.

ConveyorPictureOptions

interface ConveyorPictureOptions

What to generate.

  • seed: number — Seed, a whole number from 0 to 4294967295; the same options give the same picture.
  • width: number — Width in cells, 4 to 64.
  • height: number — Height in cells, 4 to 64.
  • colors?: number — Colours used, 2 to 12; default 4.
  • style?: ConveyorPictureStyle — Kind of picture; default 'sprite'.
  • palette?: readonly string[] — CSS colours to draw from, at least colors of them; default CONVEYOR_PALETTE.
  • fill?: boolean — Sprite only: fill the cells round the figure with a background colour (the picture is then a full rectangle); default true.

ConveyorPictureStyle

type ConveyorPictureStyle = 'sprite' | 'blobs' | 'rings' | 'stripes'

Kinds of generated picture: 'sprite' a mirrored figure with an outline, 'blobs' patches of colour, 'rings' concentric bands, 'stripes' straight bands.

ConveyorPlay

interface ConveyorPlay

How a played level ended, from playConveyor or replayConveyor.

  • won: boolean — Whether every cube was cleared.
  • launches: string[] — The shooters tapped, in order, each when the belt was empty.
  • benchPeak: number — Most shooters on the bench at once.
  • seconds: number — Seconds of play the run took.

ConveyorPoint

interface ConveyorPoint

A point on the belt in board cell units (the board spans x 0..width, y 0..height, y down), facing its travel.

  • x: number — Across, in cells from the board's left edge; negative or past width beside the board.
  • y: number — Down, in cells from the board's top edge; negative or past height beside the board.
  • angle: number — Direction of travel in radians, 0 along +x, clockwise positive on screen.
  • side: ConveyorSide | null — The side being passed, or null on a corner.

ConveyorRefusal

type ConveyorRefusal = | 'not-playing' | 'unknown-shooter' | 'not-at-front' | 'link-not-at-front' | 'not-queued' | 'no-tray' | 'not-lost' | 'bad-count'

Why an action was refused.

ConveyorResult

type ConveyorResult = { valid: true; reason?: undefined; events: ConveyorEvent[] } | { valid: false; reason: ConveyorRefusal; events: [] }

The answer to an action: done with its events (and no reason), or refused with a reason and no change.

ConveyorRider

interface ConveyorRider

A shooter on the belt.

  • id: string — The shooter's id.
  • pos: number — Distance travelled from the gate this lap, in stations.
  • next: number — Index in the track's stations of the next one it will pass.

ConveyorRuleOptions

type ConveyorRuleOptions = Omit<ConveyorSortConfig, 'width' | 'height' | 'cells' | 'columns'>

The rules a generated level plays by: everything in a config but the picture and the columns.

ConveyorShooter

interface ConveyorShooter

A shooter waiting in a column.

  • id: string — Unique id, 1 to 100 characters; events and actions name it.
  • color: string — Colour, a CSS colour string: it hits only cubes of exactly this string.
  • ammo: number — Shots it carries, 1 to 9999. It leaves for good when they run out.
  • link?: string — Shooters with the same link name go on the belt together and only when every one of them is at the front of its column. The link holds while they queue; on the bench they are separate.

ConveyorSide

type ConveyorSide = 'bottom' | 'right' | 'top' | 'left'

A side of the board that a conveyor runs along.

ConveyorSortConfig

interface ConveyorSortConfig

A conveyor sort level: the picture, the columns of shooters and the rules.

  • width: number — Board width in cells, 1 to 64.
  • height: number — Board height in cells, 1 to 64.
  • cells: ConveyorCell[] — The picture: cubes on distinct cells inside the board, 1 to 4096. Empty cells are allowed anywhere.
  • columns: ConveyorShooter[][] — The shooters, one array per column, front first: 1 to 12 columns. Only a column's front shooter can be sent. Each colour's ammo must be at least the hits its cubes take, so no level is lost for want of shots.
  • trays?: number — Trays at the gate: how many shooters can be on the belt or waiting to get on at once, 1 to 20; default 5.
  • bench?: number — Bench slots for shooters that finish a lap with shots left, 0 to 16; default 5. A return to a full bench loses.
  • speed?: number — Belt speed in stations per second, 1 to 200; default 24.
  • spacing?: number — Least gap between shooters getting on, in stations, 0.25 to 20; default 1.5.
  • tickRate?: number — Simulation steps per second, 30 to 960; default 120. Rules move only in whole steps.
  • lineOfFire?: ConveyorLineOfFire — 'first' (default): a shooter hits the outermost cube in the line only if it is its colour, so the picture peels from the outside. 'first-match': it hits the nearest cube of its colour, shooting past the others.
  • burst?: number — Shots a shooter fires at one station as it passes, 1 to 8; default 1.
  • track?: ConveyorTrackOptions — The belt's layout: gate corner, direction, firing sides and corner length.
  • seed?: number — Seed for the shuffle booster, a whole number from 0 to 4294967295; default 1.

ConveyorState

interface ConveyorState

Everything that changes in play; a save holds exactly this.

  • hits: number[] — Hits left on each cell, row-major (y * width + x); 0 is an empty cell.
  • columns: string[][] — Shooter ids still queued, one array per column, front first.
  • links: Record<string, string> — Link names of queued shooters that still have one.
  • ammo: Record<string, number> — Shots left, by shooter id.
  • staging: string[] — Shooters sent and holding a tray, waiting for room to get on the belt, first first.
  • belt: ConveyorRider[] — Shooters on the belt, in the order they got on.
  • bench: (string | null)[] — The bench, one entry per slot: a shooter id or null for a free slot.
  • trays: number — Trays at the gate now (boosters add more).
  • status: 'playing' | 'won' | 'lost' — 'playing', 'won' (every cube cleared) or 'lost' (a shooter came back to a full bench).
  • held: string | null — The shooter stopped at the gate when the level was lost; null otherwise.
  • tick: number — Simulation steps run.
  • carry: number — Seconds given to advance and not yet run as a whole step.
  • shots: number — Shots fired.
  • launches: number — Times a shooter was sent, from a column or the bench.
  • benchPeak: number — Most shooters on the bench at once so far.
  • rng: number[] — The shuffle booster's random stream.

ConveyorStation

interface ConveyorStation

A place on the belt level with one row or column of the board, where a passing shooter fires inward once.

  • side: ConveyorSide — The side of the board it is on.
  • index: number — The column (top and bottom sides) or row (left and right sides) it fires along, from 0.
  • at: number — Its distance along the belt from the gate, in station units.
  • fires: boolean — False on a side that does not fire.

ConveyorTrack

interface ConveyorTrack

The belt's layout: the order of its sides and stations and its length. Get one from conveyorTrack.

  • width: number — Board width in cells.
  • height: number — Board height in cells.
  • length: number — One lap's length in station units: two widths, two heights and four corners.
  • gap: number — Station units round each corner.
  • sides: ConveyorSide[] — The sides in the order a shooter passes them.
  • stations: ConveyorStation[] — Every station in the order a shooter passes it, with nondecreasing at.
  • corners: { corner: ConveyorCorner; at: number }[] — The corners in the order a shooter reaches them, the gate first at 0, with their distance along the belt.

ConveyorTrackOptions

interface ConveyorTrackOptions

How the conveyor is laid round the board. Every field is optional.

  • gate?: ConveyorCorner — The corner where shooters get on and off; default 'bottom-left'.
  • direction?: 'clockwise' | 'counterclockwise' — Which way the belt runs as seen on screen (y down); default 'counterclockwise', which from a bottom-left gate goes along the bottom to the right first, then up the right side, along the top and down the left side.
  • firing?: ConveyorSide[] — The sides whose stations fire; the belt still carries shooters past the others. Default all four.
  • cornerGap?: number — Track length round each corner, in station units, 0 to 8; default 1.