Phaser AE

Pipe and network puzzles

Pro packv3.0.0-32DGrid

Two tile-connection puzzles share one idea here: a tile is a set of edges, and connection is a flood from a source through edges that match on both sides. One game rotates tiles until a network connects; the other lays pipes ahead of water on the clock.

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

Pipe and network 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

netflow

function netflow(): Pack<{ netflow: NetflowApi }>

The netflow pack. Pure logic, so it needs no backend and runs headless.

Helpers

createFlowGrid

function createFlowGrid(cfg: FlowConfig): FlowGrid

Creates an edge-mask tile grid from the config. Throws on a size that is not a whole number of at least 1.

createPipeRun

function createPipeRun(options: PipeRunOptions): PipeRun

Create a Pipe Mania run on a cols by rows board. The first round starts at once (its water waiting out the delay); call step(dt) from the game's step, place(x, y) on a click, and next() when a round is over. Throws on a board smaller than 2 by 2, bad lives, hurry or ladder.

DOWN

const DOWN: 4

LEFT

const LEFT: 8

PIPE_LADDER

const PIPE_LADDER: Readonly<PipeLadder>

The default ladder: 8 pipes after 15 seconds at 2 seconds a pipe, then 2 more pipes, a second less of wait, 0.15 s faster water and one more obstacle each level, until 30 pipes, 5 s, 0.5 s and 10 obstacles.

PIPE_PIECES

const PIPE_PIECES: readonly PipePiece[]

The seven Pipe Mania pieces: two straights, four corners and the cross, the cross dealt half as often.

PIPE_SCORING

const PIPE_SCORING: Readonly<PipeScoring>

The default scoring: 10 a pipe, 20 past the target, 50 for a crossing and 200 for a drain; 5 off a replacement and 10 off each pipe left dry.

pipeAutopilot

function pipeAutopilot(run: PipeRun, options?: PipeAutopilotOptions): PipeAutopilot

An autopilot for a run (see PipeAutopilot).

pipeLevel

function pipeLevel(level: number, ladder: Partial<PipeLadder> = {}): PipeLevel

The round settings for a level (1 and up) on a ladder (over PIPE_LADDER, rung by rung). Target and obstacles are rounded to whole numbers; the delay is at least 0 and the speed above 0. Throws on a level that is not a whole number of at least 1, or a rung that is not finite.

const RIGHT: 2

UP

const UP: 1

The four edge bits of a tile mask: UP=1, RIGHT=2, DOWN=4, LEFT=8. A mask is the sum of the edges a tile opens on.

Classes

FlowGrid

class FlowGrid

An edge-mask tile grid for Netwalk and Pipe Mania. Create one with createFlowGrid().

  • readonly cols: number — The number of columns in the grid.
  • readonly rows: number — The number of rows in the grid.
  • queue: number[] — Pipe Mania: the upcoming pieces, as masks; placePipe takes the first, and the queue refills from its pieces.
  • filledDist: number — Pipe Mania: the number of pipes the water has filled, the start included.
  • constructor(cfg: FlowConfig)
  • tile(x: number, y: number): Tile — Returns the tile at the cell (the live object: change it through setTile, rotate and lock). The cell must be in bounds.
  • setTile(x: number, y: number, t: Tile): void — Sets the tile at the cell to a copy of t. A kind: 'source' tile becomes the source.
  • get serverPos(): XY | null — Netwalk: the server cell, or null before a puzzle is generated or a source tile is set.
  • rotate(x: number, y: number, steps = 1): void — Rotate a tile (Netwalk) by quarter turns: +1 clockwise, -1 back. Ignored if locked. Rotating back is the undo.
  • lock(x: number, y: number, on: boolean): void — Lock or unlock a tile: a Netwalk misclick guard, or a Pipe Mania obstacle.
  • connectivity(): { lit: boolean[]; litCount: number } — Cells connected to the source through stubs that match on both sides (the lit network), as a flag per cell (y * cols + x) and a count.
  • get solved(): boolean — Solved (Netwalk) when every tile with any stub is lit and no stub points off the board or at a neighbour without a stub back. A generated board has exactly one solution up to tiles whose turns look the same (straights, crosses).
  • generateNet(options: NetOptions = {}): void — Generate a solvable Netwalk: grow a random spanning tree over every cell from the server, set the edge masks, record that solution, then turn every tile a random number of quarter turns. Always solvable, by construction. Throws on a branching outside 0 to 1 or a source off the board.
  • solveNet(): number[][] | null — Clockwise quarter turns per cell, [y][x], that bring a generated Netwalk back to its recorded solution (for autoplay and hints). Null if the board was not generated.
  • startPipes(options: PipeOptions = {}): void — Start a Pipe Mania round on this board: every cell is emptied except locked ones (obstacles, and fixed pipes you set and locked), the last round's start, drain and scattered obstacles are cleared, the start pipe goes at start (or a random cell) with one stub on exit, any drain and obstacles are placed, and the queue is filled. Call advance(dt) from the game's step to run the delay and the flow, or advanceFlow() to move the water one pipe yourself. Throws on a start off the board or on a locked cell, an exit that is not one edge, a drain off the board, on the start or facing off the board, out-of-range timings, pieces, obstacles or scoring, or a board with no room for a random start or drain.
  • placePipe(x: number, y: number): PipePlacement — Pipe Mania: put the first queued piece at a cell, empty or holding a pipe the water has not reached (which it replaces, at the replacement cost), and refill the queue. It refuses outside, locked (an obstacle, a fixed pipe, the start or a drain), filled (the water has filled the pipe or is filling it now) and ended.
  • advanceFlow(): FlowStep — Advance the water one pipe: it fills the pipe it is in, then moves into the neighbour its exit faces. A cross is passed straight through (and can be filled a second time on its other axis); any other pipe leaves by its other stub. It spills off the board, into a cell with no stub facing it, or into a pipe it has already filled that way, and finishes when it fills a drain.
  • advance(dt: number): FlowStep[] — Pipe Mania's clock: run the round on by dt seconds of game time. The water waits out delay, then fills one pipe every secondsPerPipe; returns the pipe steps that happened, in order (several when dt is long). Nothing happens before startPipes or once the round is over.
  • hurry(secondsPerPipe: number): void — Pipe Mania: hurry the water (the "flooz" button): from now on it fills one pipe every secondsPerPipe seconds, starting at once if it was still waiting.
  • get pipeStatus(): PipeStatus | null — Pipe Mania: where the round stands, or null before startPipes.
  • suggestPipe(): XY | null — Pipe Mania: where to lay the first queued piece, for hints and autopilots, or null when there is nothing worth doing (no round, the round over, the route already joined to a drain, or no cell to put it on). It follows the water's route from where it is to the first open end. The piece goes there when it fits (it takes the water in and leads on to a free cell or a matching stub) and, with a drain on the board, when it leads no farther from the drain than the route needs to wander to make its target; a dry misfit at the open end is replaced. A piece that is no use goes where it does least harm, far from the open end and the drain: over a dry pipe off the route when a replacement costs less than a pipe left dry (so one junk pipe takes every misfit), else on an empty cell, else over a dry pipe. Greedy: one piece at a time, with no look-ahead through the queue.
  • isFilled(x: number, y: number): boolean — Returns true when the water has filled the pipe at the cell.
  • filledEdges(x: number, y: number): number — The edges the water has run through at the cell, as a mask: one axis of a cross it has filled once, both once it has crossed. 0 when dry.
  • debugMasks(): string — Returns the grid as text: one hex digit per tile mask. For tests.

PipeAutopilot

class PipeAutopilot

A player for a run: attract modes, demos, and a test that a ladder can be climbed. Every pace seconds it lays the piece in hand where suggest() says; once the laid route will fill the target (with margin to spare, or into the drain) it stops laying, since every piece more would only be left dry, and flushes the water on.

  • constructor(private readonly run: PipeRun, options: PipeAutopilotOptions = {})
  • act(dt: number): PipeAct | null — Play dt seconds: returns what it did, or null. Call it before the run's step.

PipeRun

class PipeRun

A Pipe Mania game: rounds on one board, levels, lives and a total. Create one with createPipeRun().

  • readonly grid: FlowGrid — The board; read tiles, the queue and pipeStatus from it.
  • level: number — The level being played (or just played).
  • constructor(options: PipeRunOptions)
  • get round(): PipeStatus — The round's state (see PipeStatus).
  • get lives(): number — Rounds that may still be lost.
  • get total(): number — Points banked from finished rounds.
  • get score(): number — The total with the current round's points.
  • get phase(): PipeRunPhase — Where the run stands.
  • restart(): void — Back to the first level with every life and no points, and start its round.
  • next(): boolean — Start the next round once this one is over: the next level after a win, the same level again after a loss. Returns false (and does nothing) while a round is playing or when the run is over.
  • place(x: number, y: number): PipePlacement — Lay the first queued piece at a cell (see FlowGrid.placePipe).
  • hurry(): void — Send the water on at the run's hurry speed.
  • suggest(): XY | null — Where to lay the first queued piece (see FlowGrid.suggestPipe).
  • get requireDrain(): boolean — Whether a round is won only in its drain.
  • step(dt: number): FlowStep[] — Run the round on by dt seconds; when it ends, its points are banked and a lost round costs a life. Returns the pipe steps.

Types

FlowConfig

interface FlowConfig

Configuration for createFlowGrid().

  • cols: number — The number of columns in the grid, at least 1.
  • rows: number — The number of rows in the grid, at least 1.
  • seed?: number — Seed of the generators (Netwalk boards and Pipe Mania's queue), an unsigned 32-bit integer; the same seed gives the same puzzle. Default 1.
  • wrap?: boolean — Whether edges join across the border (a torus), for connection and flow alike. Default false.

FlowStep

interface FlowStep

What one pipe of flow did.

  • entered: XY | null — The cell the water filled, or tried to enter when it spilled; null before the flow has started.
  • spilled: boolean — True when the flow is over: the water ran out of pipe (off the board, into an empty or unconnected cell, or back into a pipe it has filled) or reached a drain (drained); and on every call after.
  • drained: boolean — True when this step finished in a drain.
  • dist: number — Pipes filled so far, the start included.
  • points: number — Points this step scored: its pipe and any crossing and, on the step that ends the round, the drain bonus less the cost of the pipes left dry.

NetflowApi

type NetflowApi = typeof kit

game.netflow — every function and type this pack carries, as one namespace.

NetOptions

interface NetOptions

Options for generateNet.

  • branching?: number — How bushy the network is, 0 to 1: 0 (the default) grows one long winding line at a time (depth-first), 1 branches everywhere (like Prim's algorithm), between mixes them.
  • source?: XY — The server's cell. Default a random cell.

PipeAct

type PipeAct = { kind: 'place'; at: XY; placement: PipePlacement } | { kind: 'hurry' }

What an autopilot did in one act.

PipeAutopilotOptions

interface PipeAutopilotOptions

Options for pipeAutopilot().

  • pace?: number — Seconds between pieces, above 0: about a person's pace by default. Default 0.8.
  • margin?: number — Pipes past the target the route should run before the autopilot stops laying and flushes the water on, at least 0. Default 2.

PipeDrain

interface PipeDrain

A Pipe Mania drain: a fixed end pipe with one stub, entry, facing the cell the water must arrive from.

  • at: XY — The drain's cell.
  • entry: number — Its stub (UP, RIGHT, DOWN or LEFT): the side the water comes in by.

PipeLadder

interface PipeLadder

The settings a level sets, each a rung.

  • target: PipeRung — Pipes to fill (whole).
  • delay: PipeRung — Seconds before the water starts.
  • secondsPerPipe: PipeRung — Seconds the water takes per pipe.
  • obstacles: PipeRung — Obstacles scattered at random (whole).

PipeLevel

interface PipeLevel

A level's round settings.

  • target: number — Pipes to fill.
  • delay: number — Seconds before the water starts.
  • secondsPerPipe: number — Seconds per pipe.
  • obstacles: number — Obstacles scattered.

PipeOptions

interface PipeOptions

Options for startPipes.

  • start?: XY — The start pipe's cell. Default a random free cell whose exit faces another free cell (away from the border when it can be).
  • exit?: number — The edge the water leaves the start pipe by (UP, RIGHT, DOWN or LEFT). Default, with a random start, a random edge facing a free cell; required with start.
  • drain?: PipeDrain | boolean — A drain the water can finish in, or true for one at random, a third of the board or more from the start. Default none.
  • requireDrain?: boolean — Won only by reaching the drain with the target filled; a spill anywhere else loses the round. Default false: a drain is a bonus.
  • obstacles?: number — Obstacles to scatter this round at random, beside any cells you locked, at least 0. They clear at the next startPipes. Default 0.
  • scoring?: Partial<PipeScoring> — What the round scores, over PIPE_SCORING.
  • queue?: number[] — Pieces to deal first, as masks, in order; after them the queue deals from pieces. Default none.
  • pieces?: readonly PipePiece[] — What the queue deals, by weight. Default PIPE_PIECES.
  • preview?: number — How many upcoming pieces queue shows, at least 1. Default 5.
  • delay?: number — Seconds before the water starts, at least 0. Default 15.
  • secondsPerPipe?: number — Seconds the water takes to fill one pipe, above 0. Default 2.
  • target?: number — Pipes the water must fill for the round to be won, at least 1. Default 10.

PipePiece

interface PipePiece

A Pipe Mania piece and how often the queue deals it.

  • mask: number — Its edge mask: two edges for a straight or corner, all four (15) for a cross the water passes straight through.
  • weight: number — Relative chance of being dealt, above 0.

PipePlacement

interface PipePlacement

What placePipe did.

  • placed: boolean — True when a piece went down.
  • replaced: number — The mask of the unfilled pipe it replaced, or 0 when the cell was empty.
  • points: number — Points this scored: the replacement cost, negated, or 0.
  • reason?: 'outside' | 'locked' | 'filled' | 'ended' — Why it did not: outside, locked (an obstacle, a fixed pipe, the start or a drain), filled (the water has filled it or is filling it) or ended.

PipeRung

interface PipeRung

How one round setting moves with the level: start at level 1, then step a level, never past limit.

  • start: number — The value at level 1.
  • step: number — Added each level (negative to fall).
  • limit: number — Where it stops: a ceiling for a rising value, a floor for a falling one.

PipeRunOptions

interface PipeRunOptions

Options for createPipeRun().

  • cols: number — The board's columns, at least 2.
  • rows: number — The board's rows, at least 2.
  • seed?: number — Seed of the board's dealing and layouts, an unsigned 32-bit integer. Default 1.
  • wrap?: boolean — Edges join across the border. Default false.
  • lives?: number — Rounds that may be lost before the run is over, a whole number of at least 1 or Infinity. Default 3.
  • level?: number — The level the run starts on. Default 1.
  • ladder?: Partial<PipeLadder> — How the levels climb, over PIPE_LADDER.
  • scoring?: Partial<PipeScoring> — What a round scores, over PIPE_SCORING.
  • pieces?: readonly PipePiece[] — What the queue deals. Default PIPE_PIECES.
  • preview?: number — Pieces shown ahead. Default 5.
  • drain?: boolean — A drain at random each round. Default false.
  • requireDrain?: boolean — The drain must be reached to win a round (with drain). Default false.
  • hurry?: number — Seconds per pipe once the player hurries the water. Default 0.08.
  • round?: (level: number, grid: FlowGrid) => PipeOptions | void — Per-level overrides, for authored layouts: called before each round with the level and the board, after the last round is cleared; lock cells or lay fixed pipes on it here, and return any round options to set (a start and exit, a drain, a target …), which win over the ladder's.

PipeRunPhase

type PipeRunPhase = 'playing' | 'won' | 'lost' | 'over'

Where a run stands: a round playing, between rounds after one was won or lost, or over (no lives left).

PipeScoring

interface PipeScoring

What a Pipe Mania round scores. Points are whole or fractional; a negative value turns a bonus into a cost.

  • pipe: number — Each pipe the water fills up to the round's target, the start and drain aside; a cross scores each time it is filled.
  • extra: number — Each pipe it fills past the target, in place of pipe.
  • crossing: number — Added when it fills a cross the second time, on the other axis.
  • replacement: number — Taken off for each piece laid over an unfilled pipe.
  • unused: number — Taken off when the round ends, for each pipe laid that the water never reached.
  • drain: number — Added when the water reaches a drain and the round is won.

PipeStatus

interface PipeStatus

Where a Pipe Mania round stands.

  • phase: 'waiting' | 'flowing' | 'spilled' | 'won' — waiting for the delay, flowing, or over: spilled short of the target, or won (the water reached target pipes before spilling).
  • countdown: number — Seconds until the water starts (0 once it has).
  • filled: number — Pipes filled, the start included.
  • target: number — Pipes the round needs.
  • crossings: number — Pipes the water passed through twice, once each way (crosses).
  • replacements: number — Pieces placed over an unfilled pipe.
  • score: number — The round's score so far; once it is over, with the drain bonus and the cost of the dry pipes settled.
  • unused: number — Pipes laid that the water has not reached (fixed, locked pipes aside).
  • drained: boolean — True once the water has reached a drain.
  • head: XY | null — The cell the water is filling (the start while it waits), or null once the round is over.
  • entry: number — The edge the water comes into head by, 0 at the start: with the tile's mask, which way it runs through.
  • progress: number — How far through filling head the water is, 0 to 1: draw a pipe filling with it. 0 while it waits.
  • ahead: number — Pipes laid on the water's route beyond head, up to its open end or into a drain (a cross it will pass twice counts twice): what it will fill if nothing changes. 0 once the round is over.
  • reachesDrain: boolean — True when that route runs into a drain.

Tile

interface Tile

One grid tile: an edge mask plus an optional role.

  • mask: number — 4-bit edge mask of connection stubs (see UP, RIGHT, DOWN, LEFT); 0 is an empty cell.
  • kind?: 'source' | 'drain' | 'normal' — 'source' is where power or water starts (Netwalk's server, Pipe Mania's start pipe); 'drain' is a Pipe Mania end pipe, which finishes the round when the water reaches it; default 'normal'.
  • locked?: boolean — Netwalk: the tile does not turn. Pipe Mania: nothing can be placed on it (an obstacle when its mask is 0).

XY

interface XY

A grid cell position. x is the column and y is the row, from the top left.

  • x: number — The column, from the left.
  • y: number — The row, from the top.