The pack
levelgen
function levelgen(): Pack<{ levelgen: LevelgenApi }>
The level generators pack: Maze (ten algorithms, braid, rooms, exits, queries), Dungeon (rooms, corridors, doors, tags), Cave (cellular automata, largest region, farthest pair). Headless; each returns a thick 0/1 tile grid for tiles-2d.
Classes
Cave
class Cave
A generated cave: the tile grid plus the queries a game needs. Construct once (generation runs in the constructor); read from it every frame.
const cave = new Cave({ cols: 80, rows: 50, seed: 'cavern-1', fillProb: 0.46 });
const grid = cave.tiles(); // 0 floor / 1 wall → CollisionGrid
const [entrance, exit] = cave.farthestPair(); // two far-apart floor cells
readonly cols: number— Grid width in tiles.readonly rows: number— Grid height in tiles.readonly seed: number— The numeric seed actually used.readonly grid: Uint8Array— Per-tile data, row-major:1wall,0floor. PreferisWall/tiles, but exposed for custom exporters.constructor(opts: CaveOptions)index(col: number, row: number): number— Row-major tile index for(col, row). No bounds check.inBounds(col: number, row: number): boolean— Is(col, row)inside the grid?isWall(col: number, row: number): boolean— Is the tile solid rock? Out-of-bounds reads as wall.isFloor(col: number, row: number): boolean— Is the tile open (walkable) cavern floor?cellAt(x: number, y: number, cellSize = 1, originX = 0, originY = 0): Cell | null— Which cell contains world/pixel position(x, y)atcellSizeunits per tile from(originX, originY).nullwhen outside the grid.get openCount(): number— Count of open floor tiles.floors(): Cell[]— Every open floor cell — for scattering spawns / ore / pickups.farthestPair(): [Cell, Cell] | []— Two far-apart floor cells (an approximate cave "diameter"), good for an entrance/exit pair. Returns[]if there's no floor.path(from: Cell, to: Cell): Cell[]— Shortest walkable path fromfromtoto(4-dir BFS through floor), inclusive;[]if unreachable.tiles(opts: CaveTileOptions = {}): number[][]— The cave as a THICK 0/1 tile grid (grid[y][x]), ready for aTilemap/CollisionGrid/ wall-box build.toString(): string— ASCII rendering (#wall, space floor) — tests + console.
Dungeon
class Dungeon
A generated dungeon: the tile grid plus room/door metadata and the queries a game needs. Construct once (generation runs in the constructor); read from it every frame.
const dungeon = new Dungeon({ cols: 60, rows: 40, seed: 'level-1', extraConnections: 3 });
const grid = dungeon.tiles(); // 0 floor / 1 wall → CollisionGrid
const start = dungeon.roomsByTag('start')[0]; // where to drop the player
const boss = dungeon.roomsByTag('boss')[0]; // the objective room
readonly cols: number— Grid width in tiles.readonly rows: number— Grid height in tiles.readonly seed: number— The numeric seed actually used.readonly grid: Uint8Array— Per-tile data, row-major:1wall,0floor,2door. PreferisWall/tiles, but exposed for custom exporters.readonly rooms: DungeonRoom[]— Every placed room.readonly doors: Cell[]— Detected doorway tiles (1-wide thresholds into rooms).readonly connections: Array<[number, number]>— Room-graph edges as[roomId, roomId]pairs (which rooms are joined).constructor(opts: DungeonOptions)index(col: number, row: number): number— Row-major tile index for(col, row). No bounds check.inBounds(col: number, row: number): boolean— Is(col, row)inside the grid?isWall(col: number, row: number): boolean— Is the tile solid wall? Out-of-bounds reads as wall.isFloor(col: number, row: number): boolean— Is the tile walkable (floor or door)?cellAt(x: number, y: number, cellSize = 1, originX = 0, originY = 0): Cell | null— Which cell contains world/pixel position(x, y)atcellSizeunits per tile from(originX, originY).nullwhen outside the grid.roomAt(col: number, row: number): DungeonRoom | null— The room whose rectangle contains(col, row), ornull(a corridor / wall).roomsByTag(tag: RoomTag): DungeonRoom[]— All rooms carrying a givenRoomTag.floors(): Cell[]— Every walkable (floor or door) cell — for scattering pickups / spawns.path(from: Cell, to: Cell): Cell[]— Shortest walkable path fromfromtoto(4-dir BFS through floor + doors), inclusive;[]if unreachable.tiles(opts: DungeonTileOptions = {}): number[][]— The dungeon as a THICK 0/1 tile grid (grid[y][x]), ready for aTilemap/CollisionGrid/ wall-box build. Doors default to the floor value.toString(): string— ASCII rendering (#wall,+door,.floor) — tests + console.
Maze
class Maze
A generated 2D maze: the wall grid plus the geometry exporters and gameplay queries a game needs. Construct once (generation runs in the constructor); the instance is then immutable data you read from every frame.
const maze = new Maze({ cols: 20, rows: 15, algorithm: 'prim', seed: 'level-1', braid: 0.3 });
const grid = maze.tiles(); // thick walls → CollisionGrid / Tilemap
if (maze.canMove(c, r, E)) c++; // gameplay movement test
const dir = maze.directionToExit(c, r); // hint arrow toward nearest exit
static readonly N: Dir— North direction index (row − 1).static readonly E: Dir— East direction index (col + 1).static readonly S: Dir— South direction index (row + 1).static readonly W: Dir— West direction index (col − 1).readonly cols: number— Grid width in cells.readonly rows: number— Grid height in cells.readonly algorithm: MazeAlgorithm— Algorithm used to carve this maze.readonly seed: number— The numeric seed actually used (after hashing a string seed).readonly cells: Uint8Array— Per-cell wall bitmask, row-major (cells[row * cols + col]). A set bit means the wall is PRESENT: N=1, E=2, S=4, W=8.@internal-ish — preferwall/canMove, but exposed for custom exporters.readonly exits: Exit[]— The resolved border exits (entrances and exits are interchangeable).readonly rooms: Room[]— The open rooms that were stamped (empty whenroomswas not requested).constructor(opts: MazeOptions)index(col: number, row: number): number— Row-major cell index for(col, row). No bounds check.inBounds(col: number, row: number): boolean— Is(col, row)inside the grid?wall(col: number, row: number, dir: Dir): boolean— Is there a wall on thedirside of cell(col, row)? Out-of-bounds cells read as fully walled.canMove(col: number, row: number, dir: Dir): boolean— Can an actor in cell(col, row)step indir? True when no wall blocks that side. Stepping through a border opening (anExit) counts as a move that LEAVES the maze, so this returns true there too — checkinBoundson the target if you need to distinguish "exited".step(col: number, row: number, dir: Dir): Cell— The cell reached by steppingdirfrom(col, row)(may be out of bounds).neighbours(col: number, row: number): Array<Cell & { dir: Dir }>— Open (wall-free) in-bounds neighbours of(col, row), each with thedirtaken to reach it.cellAt(x: number, y: number, cellSize = 1, originX = 0, originY = 0): Cell | null— Which cell contains pixel/world position(x, y)? Maps by the thin-wall layout where one cell spanscellSizeunits from(originX, originY). Returnsnullwhen the point is outside the grid. (For a THICKtilesrender, cell(c, r)is tile(2c+1, 2r+1).)tiles(opts: TileOptions = {}): number[][]— Expand to a THICK 0/1 wall grid of size(2·cols+1) × (2·rows+1), row-major (grid[y][x]). Cell centres and open passages become floor; walls and the pillars between them stay solid. Feed straight into aTilemap/CollisionGrid, or place one wall box per solid tile for a Rogue-style 3D dungeon. Exits appear as gaps punched in the outer ring.walls(): WallSeg[]— The maze as THIN wall segments in cell units (0…cols×0…rows) — the puzzle-book look. Each internal edge appears once; border edges are included except where an exit opens. Draw each as a line (d.line(x1*scale, y1*scale, x2*scale, y2*scale, ...)).distanceField(targets: Cell[]): Int32Array— BFS distance (in cells, through open passages) from every cell to the NEAREST oftargets, as a row-majorInt32Array. Unreachable cells are-1. Cheap to reuse: compute one field toward a goal, then read a direction from any cell in O(1) withdirectionTo.path(from: Cell, to: Cell): Cell[]— Shortest path fromfromtotoas an inclusive list of cells ([from … to]), or[]when unreachable. For repeated queries toward one goal, build adistanceFieldonce and step it withdirectionTo.directionTo(col: number, row: number, goal: Cell | Int32Array): Dir | -1— The firstDirto step from(col, row)to get closer to a goal, or-1if there's nowhere better to go. Pass a precomputeddistanceFieldto reuse it across cells/frames; pass a singleCellfor a one-off.exitField(): Int32Array— The distance-to-nearest-exit field (built once, then cached).-1where no exit is reachable, or everywhere when the maze has no exits.directionToExit(col: number, row: number): Dir | -1— TheDirto step from(col, row)toward the nearestExit, or-1if already at an exit / none is reachable.deadEnds(): Cell[]— Every dead-end cell (exactly one open side). Handy for placing treasure, keys, or spawn points.toString(): string— A compact ASCII rendering (#walls, space floor) — for tests and console debugging.
Types
CaveOptions
interface CaveOptions
Configuration for a new Cave. cols/rows are required.
cols: number— Grid width in tiles.rows: number— Grid height in tiles.seed?: number | string— Seed for reproducible caves (string is hashed). Omit for random.fillProb?: number— Initial wall probability, 0…1. Higher = tighter caves. Default0.45.iterations?: number— Cellular-automata smoothing passes. More = rounder. Default4.wallThreshold?: number— Neighbour wall count (of 8) at/above which a cell becomes wall. Default5.keepLargest?: boolean— Discard disconnected floor pockets, keeping the largest cavern (so the cave is one connected space). Defaulttrue.
CaveTileOptions
interface CaveTileOptions
Options for Cave.tiles — the 0/1 grid.
wall?: number— Value for wall tiles. Default1.floor?: number— Value for floor tiles. Default0.
Cell
interface Cell
A grid cell.
col: number— Column.row: number— Row.
Dir
type Dir = 0 | 1 | 2 | 3
A cardinal direction as an index: N E S W = 0 1 2 3. Read the named values
off the class — Maze.N, Maze.E, Maze.S, Maze.W.
DungeonOptions
interface DungeonOptions
Configuration for a new Dungeon. cols/rows are required.
cols: number— Grid width in tiles.rows: number— Grid height in tiles.seed?: number | string— Seed for reproducible dungeons (string is hashed). Omit for random.roomAttempts?: number— How many times to try placing a room. Default scales with area.minRoom?: number— Minimum room side in tiles. Default4.maxRoom?: number— Maximum room side in tiles. Default9.roomMargin?: number— Minimum wall gap kept between rooms. Default1.extraConnections?: number— Extra corridors beyond the connecting tree, for loops. Default2.corridorWidth?: number— Corridor width in tiles. Default1(doors are only detected at width 1).
DungeonRoom
interface DungeonRoom
One generated room (a rectangle of floor), in tile coordinates.
id: number— Stable room index.col: number— Left column (inclusive).row: number— Top row (inclusive).w: number— Width in tiles.h: number— Height in tiles.cx: number— Centre column (floored).cy: number— Centre row (floored).tag: RoomTag— Role in the layout (seeRoomTag).
DungeonTileOptions
interface DungeonTileOptions
Options for Dungeon.tiles — the 0/1(/door) grid.
wall?: number— Value for wall tiles. Default1.floor?: number— Value for floor tiles. Default0.door?: number— Value for door tiles. Default =floor(doors read as walkable floor).
Exit
interface Exit
A resolved exit: the border cell and the outward direction it opens.
col: number— Column of the border cell.row: number— Row of the border cell.dir: Dir— Outward direction through the border wall.
ExitSpec
interface ExitSpec
One requested border opening (passed in the exits array).
side: 'N' | 'E' | 'S' | 'W'— Which outer edge to open.at?: number— Position ALONG that edge (column for N/S, row for E/W). Random if omitted.
LevelgenApi
interface LevelgenApi
game.levelgen. Seeds default to the game's seed so a level replays; pass seed for one of your own.
maze(opts: MazeOptions): Maze— A maze: wall-per-edge corridors.dungeon(opts: DungeonOptions): Dungeon— A dungeon: rooms joined by corridors, doors, tagged rooms.cave(opts: CaveOptions): Cave— A cave: cellular-automata caverns.
MazeAlgorithm
type MazeAlgorithm = | 'backtracker' | 'prim' | 'kruskal' | 'wilson' | 'aldous-broder' | 'hunt-and-kill' | 'eller' | 'recursive-division' | 'binary-tree' | 'sidewinder'
Which carving algorithm to run. Each biases the maze's look:
backtracker(default) — long, winding corridors, few branches ("rivery").prim/kruskal— short branches, bushy, many junctions; look alike.wilson/aldous-broder— unbiased uniform spanning tree (no texture); Aldous-Broder is slow, keep it to small grids.hunt-and-kill— backtracker-ish but with more, shorter dead-ends.eller— row-at-a-time, uniform-ish, very fast.recursive-division— nested chambers, naturally roomy/rectilinear.binary-tree— trivial, strong NE diagonal bias + two open border runs.sidewinder— trivial, one fully-open top row, horizontal-run texture.
MazeOptions
interface MazeOptions
Configuration for a new Maze. cols/rows are required.
cols: number— Grid width in cells.rows: number— Grid height in cells.algorithm?: MazeAlgorithm— Carving algorithm. Default'backtracker'.seed?: number | string— Seed for reproducible mazes — same seed + options ⇒ identical maze. A string is hashed. Omit for a fresh random maze each call.braid?: number— Fraction of dead-ends (0…1) to open into loops, giving MULTIPLE routes between any two points (a "braided" maze).0= perfect maze (exactly one path anywhere);1= every dead-end removed. Default0.rooms?: RoomOptions | number— Carve open rooms into the maze. A number is shorthand for{ count }.exits?: number | ExitSpec[]— Border openings. A numbernpunchesnexits spread around the border (n=1 → one; n=2 → opposite corners; more → cycled sides). An array places them explicitly. Default2. Use0for a fully sealed maze.
Room
interface Room
A carved room rectangle, in cell coordinates (top-left inclusive).
col: number— Left column.row: number— Top row.w: number— Width in cells.h: number— Height in cells.
RoomOptions
interface RoomOptions
Open-room carving controls (passed as rooms in MazeOptions).
count?: number— How many rectangular rooms to attempt to stamp. Default3.minSize?: number— Minimum room side in cells. Default2.maxSize?: number— Maximum room side in cells. Default4.
RoomTag
type RoomTag = 'start' | 'boss' | 'treasure' | 'junction' | 'normal'
How a room reads in the layout — for placing the player, objective and loot.
TileOptions
interface TileOptions
Options for Maze.tiles — the thick 0/1 wall grid.
wall?: number— Value written for wall tiles. Default1.open?: number— Value written for open (floor) tiles. Default0.
WallSeg
interface WallSeg
A thin wall segment in cell units (0 … cols / 0 … rows), for line drawing.
x1: number— Start X in cell units.y1: number— Start Y in cell units.x2: number— End X in cell units.y2: number— End Y in cell units.


