The pack
pusher
function pusher(): Pack<{ pusher: PusherApi }>
The pusher pack. Pure logic, so it needs no backend and runs headless.
Helpers
createPusher
function createPusher(cfg: PusherConfig): Pusher
Creates a Pusher puzzle from a level. Throws on a level it cannot read (see PusherConfig.level).
parseLevels
function parseLevels(text: string): PusherLevel[]
Read a level collection in the common .sok / .xsb / .txt text form: each level is a block of rows of XSB
characters (a row is one that contains a wall and nothing but XSB characters); other lines are names and notes. The
last such line before a level names it, and a Title: ... line after one names it if nothing did. Lines starting with
; are comments. It reads rows only; whether each level is valid is createPusher's check.
Classes
Pusher
class Pusher
A Sokoban puzzle: push boxes onto goals on a grid. Create one with createPusher.
readonly cols: number— The number of columns in the level grid.readonly rows: number— The number of rows in the level grid.constructor(cfg: PusherConfig)get moves(): number— The number of player moves made so far. Undo decreases it.get pushes(): number— The number of box pushes made so far. Undo decreases it.isWall(x: number, y: number): boolean— Returns true when the cell is a wall. Cells outside the level count as walls.isGoal(x: number, y: number): boolean— Returns true when the cell is a goal square.isBox(x: number, y: number): boolean— Returns true when a box sits on the cell.isDead(x: number, y: number): boolean— Returns true when the cell is a dead square: floor inside the level from which no box can ever be pushed to a goal.get playerPos(): XY— The grid position of the player, as a copy.get metrics(): { moves: number; pushes: number }— The move and push counts, as one object.get solved(): boolean— Solved when every box sits on a goal.get deadlocked(): boolean— Whether the level can no longer be won: a box off its goal sits on a dead square or is frozen (seeisFrozen). It catches the simple and freeze deadlocks, not every lost position.get canUndo(): boolean— Whether there is a move to undo.get canRedo(): boolean— Whether there is an undone move to redo.move(dir: Dir): MoveResult— Move the player indir, pushing a single box if one is ahead. A move into a wall or a blocked box changes nothing. A move clears the redo history.undo(): boolean— Undo the last move (pulling a pushed box back). False when there is none.redo(): boolean— Redo an undone move. False when there is none.reset(): void— Return to the level's start and clear the undo and redo history.isFrozen(x: number, y: number): boolean— Is the box at (x, y) frozen: unable ever to move along either axis, because a wall, two dead squares or a box that is itself frozen the other way blocks it? A frozen box off a goal means the level is lost (deadlocked). False when no box is there.solution(maxStates = 200000): PusherSolution— Solve from the current position, for a hint or to check a level, without changing it: a breadth-first search over box positions, one push per step (the player's walk between pushes is free, so positions the player can walk between count as one), skipping pushes onto dead squares and into frozen deadlocks. The answer has the fewest pushes any solution needs, though not necessarily the fewest moves.maxStates(default 200,000) bounds the box positions kept.solve(maxStates = 200000): Dir[] | null— The moves ofsolution(maxStates)(default 200,000 box positions): fewest pushes, from the current position.[]when already solved; null when there is no solution or the budget ran out (seesolutionto tell which).debugGrid(): string— Returns the level as XSB text, one row for each line. Use it for tests and logs.
Types
Dir
type Dir = 'up' | 'down' | 'left' | 'right'
A direction the player can move in on the grid.
MoveResult
interface MoveResult
The outcome of one Pusher.move call.
moved: boolean— True when the player moved. False when a wall or a blocked box stopped the move.pushed: { from: XY; to: XY } | null— The box push made by this move — its start and end cells — or null when no box moved.ontoDeadSquare: boolean— True when the move pushed a box onto a dead square that is not a goal.deadlocked: boolean— True when the level can no longer be won after this move (seePusher.deadlocked).solved: boolean— True when every box now sits on a goal.
PusherApi
type PusherApi = typeof kit
game.pusher — every function and type this pack carries, as one namespace.
PusherConfig
interface PusherConfig
Configures createPusher with the level to load.
level: string[]— The level, one string per row, in XSB:#wall, space,-or_floor,@player,+player on a goal,$box,*box on a goal,.goal. Rows may differ in length (the rest is floor). It needs exactly one player and as many boxes as goals, at least one; any other character throws.legend?: Partial<PusherLegend>— Other characters for the same things, merged over XSB's (only the entries you give change).
PusherLegend
interface PusherLegend
Which characters of a level mean what. Each entry lists every character that means it.
wall: string— Default'#'.floor: string— Default' -_'.goal: string— Default'.'.box: string— Default'$'.boxOnGoal: string— A box on a goal; default'*'.player: string— Default'@'.playerOnGoal: string— The player on a goal; default'+'.
PusherLevel
interface PusherLevel
One level of a collection, from parseLevels.
title?: string— The level's name, when the collection gives one.level: string[]— The rows, ready forcreatePusher({ level }).
PusherSolution
interface PusherSolution
The result of Pusher.solution.
status: 'solved' | 'unsolvable' | 'budget-exhausted'—solved(with the moves),unsolvable(no push sequence wins: proven) orbudget-exhausted(no claim).moves: Dir[]— The player's moves, walking and pushing, from the current position; empty unless solved.pushes: number— Pushes inmoves: the fewest any solution needs.visited: number— Box positions examined.
XY
interface XY
A grid cell position.
x: number— The column of the cell.y: number— The row of the cell.


