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.
RIGHT
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;placePipetakes 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 throughsetTile,rotateandlock). The cell must be in bounds.setTile(x: number, y: number, t: Tile): void— Sets the tile at the cell to a copy oft. Akind: '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 abranchingoutside 0 to 1 or asourceoff 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 atstart(or a random cell) with one stub onexit, any drain and obstacles are placed, and the queue is filled. Calladvance(dt)from the game's step to run the delay and the flow, oradvanceFlow()to move the water one pipe yourself. Throws on a start off the board or on a locked cell, anexitthat 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 refusesoutside,locked(an obstacle, a fixed pipe, the start or a drain),filled(the water has filled the pipe or is filling it now) andended.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 bydtseconds of game time. The water waits outdelay, then fills one pipe everysecondsPerPipe; returns the pipe steps that happened, in order (several whendtis long). Nothing happens beforestartPipesor once the round is over.hurry(secondsPerPipe: number): void— Pipe Mania: hurry the water (the "flooz" button): from now on it fills one pipe everysecondsPerPipeseconds, starting at once if it was still waiting.get pipeStatus(): PipeStatus | null— Pipe Mania: where the round stands, or null beforestartPipes.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— Playdtseconds: returns what it did, or null. Call it before the run'sstep.
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 andpipeStatusfrom it.level: number— The level being played (or just played).constructor(options: PipeRunOptions)get round(): PipeStatus— The round's state (seePipeStatus).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 (seeFlowGrid.placePipe).hurry(): void— Send the water on at the run's hurry speed.suggest(): XY | null— Where to lay the first queued piece (seeFlowGrid.suggestPipe).get requireDrain(): boolean— Whether a round is won only in its drain.step(dt: number): FlowStep[]— Run the round on bydtseconds; 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,DOWNorLEFT): 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,DOWNorLEFT). Default, with a random start, a random edge facing a free cell; required withstart.drain?: PipeDrain | boolean— A drain the water can finish in, ortruefor 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 nextstartPipes. Default 0.scoring?: Partial<PipeScoring>— What the round scores, overPIPE_SCORING.queue?: number[]— Pieces to deal first, as masks, in order; after them the queue deals frompieces. Default none.pieces?: readonly PipePiece[]— What the queue deals, by weight. DefaultPIPE_PIECES.preview?: number— How many upcoming piecesqueueshows, 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) orended.
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 orInfinity. Default 3.level?: number— The level the run starts on. Default 1.ladder?: Partial<PipeLadder>— How the levels climb, overPIPE_LADDER.scoring?: Partial<PipeScoring>— What a round scores, overPIPE_SCORING.pieces?: readonly PipePiece[]— What the queue deals. DefaultPIPE_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 (withdrain). 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 (astartandexit, adrain, atarget…), 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 ofpipe.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'—waitingfor the delay,flowing, or over:spilledshort of the target, orwon(the water reachedtargetpipes 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 intoheadby, 0 at the start: with the tile's mask, which way it runs through.progress: number— How far through fillingheadthe water is, 0 to 1: draw a pipe filling with it. 0 while it waits.ahead: number— Pipes laid on the water's route beyondhead, 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 (seeUP,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.


