Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PROJECT_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ npm run build
| Need | Algorithm(s) | Module | Example |
| ---- | ------------ | ------ | ------- |
| Grid pathfinding | `astar`, `dijkstra`, `jumpPointSearch`, `computeFlowField`, `buildNavMesh`, `findNavMeshPath`, `manhattanDistance`, `gridFromString` | `pathfinding/astar.ts`, `pathfinding/dijkstra.ts`, `pathfinding/jumpPointSearch.ts`, `pathfinding/flowField.ts`, `pathfinding/navMesh.ts` | `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts` |
| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts` |
| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts` |
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts` |
| Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` |
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ CDN usage:
| Goal | Algorithms | Import From | Example |
| ---- | ---------- | ----------- | ------- |
| Pathfinding & navigation | `astar`, `dijkstra`, `jumpPointSearch`, `computeFlowField`, `buildNavMesh`, `findNavMeshPath`, `manhattanDistance`, `gridFromString` | `pathfinding/astar.ts`, `pathfinding/dijkstra.ts`, `pathfinding/jumpPointSearch.ts`, `pathfinding/flowField.ts`, `pathfinding/navMesh.ts` | `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts` |
| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts` |
| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts` |
| Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` |
| AI behaviours & crowds | `seek`, `flee`, `arrive`, `pursue`, `wander`, `updateBoids`, `BehaviorTree`, `rvoStep` | `ai/steering.ts`, `ai/boids.ts`, `ai/behaviorTree.ts`, `ai/rvo.ts` | `examples/steering.ts`, `examples/boids.ts`, `examples/rvo.ts` |
| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts` |
Expand All @@ -52,7 +52,7 @@ npm run size # Enforce bundle size budget
- Milestone 0.2 next targets crowd-flow integrations (RVO + flow fields) and behaviour-tree decorators for richer AI control.
- Milestone 0.4 plans a procedural + gameplay systems toolkit (Wave Function Collapse, dungeon suite, L-systems, game loop, camera, particles, inventory, combat, save/load, and more).

Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.ts`, `examples/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.
Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/flowField.ts`, `examples/navMesh.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.ts`, `examples/search.ts`, `examples/graph.ts`, `examples/geometry.ts`, `examples/visual.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts. The `examples` registry exported from `src/index.ts` provides a typed index you can traverse programmatically.

## Contributing
1. Fork the repository.
Expand Down
31 changes: 31 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const examples: {
readonly diamondSquare: 'examples/diamondSquare.ts';
readonly generateLSystem: 'examples/lSystem.ts';
readonly generateBspDungeon: 'examples/dungeonBsp.ts';
readonly generateRecursiveMaze: 'examples/mazeRecursive.ts';
};
readonly spatial: {
readonly Quadtree: 'examples/sat.ts';
Expand Down Expand Up @@ -579,6 +580,36 @@ export interface DungeonBspResult {
*/
export function generateBspDungeon(options: DungeonGeneratorOptions): DungeonBspResult;

/**
* Maze generation options for recursive backtracking.
* Use for: controlling grid dimensions and deterministic seeding.
* Import: procedural/maze.ts
*/
export interface MazeOptions {
width: number;
height: number;
seed?: number;
}

/**
* Maze generation result describing walkable grid and terminals.
* Use for: pathfinding tests, gameplay layout, puzzle generation.
* Import: procedural/maze.ts
*/
export interface MazeResult {
grid: number[][];
start: Point;
end: Point;
}

/**
* Generates a maze using recursive backtracking depth-first search.
* Use for: grid-based dungeon layouts, puzzles, procedural environments.
* Performance: O(width × height).
* Import: procedural/maze.ts
*/
export function generateRecursiveMaze(options: MazeOptions): MazeResult;

/**
* Simplex noise generator for smooth gradients without directional artifacts.
* Use for: large terrain synthesis, animated textures, volumetric noise.
Expand Down
11 changes: 11 additions & 0 deletions examples/mazeRecursive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateRecursiveMaze } from '../src/index.js';

const { grid, start, end } = generateRecursiveMaze({
width: 21,
height: 21,
seed: 314,
});

console.log('Start:', start);
console.log('End:', end);
console.log('Row preview:', grid[10]?.join(''));
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const examples = {
diamondSquare: 'examples/diamondSquare.ts',
generateLSystem: 'examples/lSystem.ts',
generateBspDungeon: 'examples/dungeonBsp.ts',
generateRecursiveMaze: 'examples/mazeRecursive.ts',
},
spatial: {
Quadtree: 'examples/sat.ts',
Expand Down Expand Up @@ -274,6 +275,13 @@ export { generateLSystem } from './procedural/lSystem.js';
*/
export { generateBspDungeon } from './procedural/dungeonBsp.js';

/**
* Recursive backtracking maze generator for grid layouts.
*
* Example file: examples/mazeRecursive.ts
*/
export { generateRecursiveMaze } from './procedural/maze.js';

// ============================================================================
// 🎯 SPATIAL & COLLISION
// ============================================================================
Expand Down
126 changes: 126 additions & 0 deletions src/procedural/maze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { createLinearCongruentialGenerator } from '../util/prng.js';
import type { Point } from '../types.js';

export interface MazeOptions {
width: number;
height: number;
seed?: number;
}

export interface MazeResult {
grid: number[][];
start: Point;
end: Point;
}

interface Cell extends Point {}

const WALL = 1;
const PATH = 0;

/**
* Generates a maze using recursive backtracking (depth-first search).
* Useful for: grid-based level layouts, puzzle generation, pathfinding tests.
* Performance: O(width × height).
*/
export function generateRecursiveMaze({
width,
height,
seed = Date.now(),
}: MazeOptions): MazeResult {
validateDimensions(width, height);

const grid = Array.from({ length: height }, () => Array<number>(width).fill(WALL));
const random = createLinearCongruentialGenerator(seed);

const start: Cell = { x: 1, y: 1 };
carveCell(grid, start);

const stack: Cell[] = [start];

while (stack.length > 0) {
const current = stack[stack.length - 1];
const neighbours = shuffledNeighbours(current, grid, random);

const next = neighbours.find((candidate) => isWall(candidate, grid));
if (next) {
const mid = {
x: current.x + (next.x - current.x) / 2,
y: current.y + (next.y - current.y) / 2,
};
carveCell(grid, mid);
carveCell(grid, next);
stack.push(next);
} else {
stack.pop();
}
}

const end = findFarthestCell(start, grid);
carveCell(grid, start);
carveCell(grid, end);

return { grid, start, end };
}

function validateDimensions(width: number, height: number): void {
if (!Number.isInteger(width) || !Number.isInteger(height)) {
throw new Error('width and height must be integers.');
}
if (width < 5 || height < 5) {
throw new Error('width and height must be at least 5.');
}
if (width % 2 === 0 || height % 2 === 0) {
throw new Error('width and height should be odd to surround cells with walls.');
}
}

function shuffledNeighbours(cell: Cell, grid: number[][], random: () => number): Cell[] {
const offsets: Array<[number, number]> = [
[0, -2],
[2, 0],
[0, 2],
[-2, 0],
];
for (let i = offsets.length - 1; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
[offsets[i], offsets[j]] = [offsets[j], offsets[i]];
}

const neighbours: Cell[] = [];
for (const [dx, dy] of offsets) {
const nx = cell.x + dx;
const ny = cell.y + dy;
if (ny > 0 && ny < grid.length && nx > 0 && nx < grid[0]!.length) {

Check failure on line 94 in src/procedural/maze.ts

View workflow job for this annotation

GitHub Actions / build

This assertion is unnecessary since it does not change the type of the expression
neighbours.push({ x: nx, y: ny });
}
}
return neighbours;
}

function isWall(cell: Cell, grid: number[][]): boolean {
return grid[cell.y]?.[cell.x] === WALL;
}

function carveCell(grid: number[][], cell: Cell): void {
if (grid[cell.y] && grid[cell.y][cell.x] !== undefined) {
grid[cell.y][cell.x] = PATH;
}
}

function findFarthestCell(start: Cell, grid: number[][]): Cell {
let farthest = start;
let maxDistance = -1;
for (let y = 1; y < grid.length; y += 2) {
for (let x = 1; x < grid[0]!.length; x += 2) {

Check failure on line 115 in src/procedural/maze.ts

View workflow job for this annotation

GitHub Actions / build

This assertion is unnecessary since it does not change the type of the expression
if (grid[y][x] === PATH) {
const distance = Math.abs(start.x - x) + Math.abs(start.y - y);
if (distance > maxDistance) {
maxDistance = distance;
farthest = { x, y };
}
}
}
}
return farthest;
}
28 changes: 28 additions & 0 deletions tests/mazeRecursive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';

import { generateRecursiveMaze } from '../src/index.js';

describe('generateRecursiveMaze', () => {
it('produces deterministic mazes for equal seeds', () => {
const options = { width: 21, height: 21, seed: 123 } as const;
const a = generateRecursiveMaze(options);
const b = generateRecursiveMaze(options);
expect(a.grid).toEqual(b.grid);
expect(a.start).toEqual(b.start);
expect(a.end).toEqual(b.end);
});

it('creates a traversable grid bounded by walls', () => {
const { grid } = generateRecursiveMaze({ width: 15, height: 15, seed: 7 });
expect(grid.length).toBe(15);
expect(grid.every((row) => row.length === 15)).toBe(true);

// Outer border should remain walls
expect(grid[0].every((cell) => cell === 1)).toBe(true);
expect(grid[grid.length - 1].every((cell) => cell === 1)).toBe(true);
expect(grid.every((row) => row[0] === 1 && row[row.length - 1] === 1)).toBe(true);

const walkableTiles = grid.flat().filter((cell) => cell === 0).length;
expect(walkableTiles).toBeGreaterThan(0);
});
});
Loading