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`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze` | `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`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts` |
| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze` | `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`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.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`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze` | `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`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts` |
| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze` | `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`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.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/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.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/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.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
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
- [x] Diamond-square terrain height map generator
- [x] L-system generator for foliage and organic structures
- [x] Dungeon generation suite (BSP subdivision, rooms & corridors variants)
- [ ] Maze algorithms pack (Recursive backtracking ✅, Prim's ✅, Kruskal's, Wilson's, Aldous–Broder, Recursive Division)
- [ ] Maze algorithms pack (Recursive backtracking ✅, Prim's ✅, Kruskal's, Wilson's, Aldous–Broder, Recursive Division)
- Gameplay systems & utilities:
- [ ] Fixed-timestep game loop utility with interpolation helpers
- [ ] Delta-time manager for frame-independent timing
Expand Down
9 changes: 9 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const examples: {
readonly generateRecursiveMaze: 'examples/mazeRecursive.ts';
readonly generatePrimMaze: 'examples/mazePrim.ts';
readonly generateKruskalMaze: 'examples/mazeKruskal.ts';
readonly generateWilsonMaze: 'examples/mazeWilson.ts';
};
readonly spatial: {
readonly Quadtree: 'examples/sat.ts';
Expand Down Expand Up @@ -628,6 +629,14 @@ export function generatePrimMaze(options: MazeOptions): MazeResult;
*/
export function generateKruskalMaze(options: MazeOptions): MazeResult;

/**
* Generates a maze using Wilson's loop-erased random walk algorithm.
* Use for: unbiased mazes matching uniform spanning trees.
* Performance: O(width × height × random walks).
* Import: procedural/maze.ts
*/
export function generateWilsonMaze(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/mazeWilson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateWilsonMaze } from '../src/index.js';

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

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 @@ -51,6 +51,7 @@ export const examples = {
generateRecursiveMaze: 'examples/mazeRecursive.ts',
generatePrimMaze: 'examples/mazePrim.ts',
generateKruskalMaze: 'examples/mazeKruskal.ts',
generateWilsonMaze: 'examples/mazeWilson.ts',
},
spatial: {
Quadtree: 'examples/sat.ts',
Expand Down Expand Up @@ -298,6 +299,13 @@ export { generatePrimMaze } from './procedural/maze.js';
*/
export { generateKruskalMaze } from './procedural/maze.js';

/**
* Wilson's maze generator using loop-erased random walks.
*
* Example file: examples/mazeWilson.ts
*/
export { generateWilsonMaze } from './procedural/maze.js';

// ============================================================================
// 🎯 SPATIAL & COLLISION
// ============================================================================
Expand Down
106 changes: 106 additions & 0 deletions src/procedural/maze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,86 @@ export function generateKruskalMaze({
return { grid, start, end };
}

/**
* Generates a maze using Wilson's loop-erased random walk algorithm.
* Useful for: unbiased mazes with uniform spanning tree properties.
*/
export function generateWilsonMaze({
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 cells: Cell[] = [];
const unvisited = new Set<string>();
for (let y = 1; y < height; y += 2) {
for (let x = 1; x < width; x += 2) {
const key = cellKey(x, y);
unvisited.add(key);
cells.push({ x, y });
}
}

const start = cells[0] ?? { x: 1, y: 1 };
carveCell(grid, start);
const startKey = cellKey(start.x, start.y);
unvisited.delete(startKey);

const tree = new Set<string>([startKey]);

while (unvisited.size > 0) {
const keys = Array.from(unvisited);
const initialKey = keys[Math.floor(random() * keys.length)];
if (!initialKey) {
continue;
}
let current = parseCellKey(initialKey);
const path: Cell[] = [current];
const visitedInWalk = new Map<string, number>([[initialKey, 0]]);

while (!tree.has(cellKey(current.x, current.y))) {
const next = randomOddNeighbour(current, grid, random);
const key = cellKey(next.x, next.y);
const existing = visitedInWalk.get(key);
if (existing !== undefined) {
path.length = existing + 1;
} else {
path.push(next);
visitedInWalk.set(key, path.length - 1);
}
current = next;
}

for (let i = 0; i < path.length; i += 1) {
const cell = path[i];
if (!cell) {
continue;
}
const key = cellKey(cell.x, cell.y);
if (!tree.has(key)) {
tree.add(key);
unvisited.delete(key);
carveCell(grid, cell);
}
if (i < path.length - 1) {
const nextCell = path[i + 1];
if (nextCell) {
carveCorridorBetween(grid, cell, nextCell);
}
}
}
}

const end = findFarthestCell(start, grid);
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.');
Expand Down Expand Up @@ -270,13 +350,39 @@ function cellKey(x: number, y: number): string {
return `${x}:${y}`;
}

function parseCellKey(key: string): Cell {
const [x, y] = key.split(':').map((value) => Number.parseInt(value, 10));
return { x, y };
}

function shuffle<T>(items: T[], random: () => number): void {
for (let i = items.length - 1; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
[items[i], items[j]] = [items[j], items[i]];
}
}

function randomOddNeighbour(cell: Cell, grid: number[][], random: () => number): Cell {
const offsets: Array<[number, number]> = [
[0, -2],
[2, 0],
[0, 2],
[-2, 0],
];
const order = offsets.slice();
shuffle(order, random);
const width = grid[0]?.length ?? 0;
const height = grid.length;
for (const [dx, dy] of order) {
const nx = cell.x + dx;
const ny = cell.y + dy;
if (ny > 0 && ny < height && nx > 0 && nx < width) {
return { x: nx, y: ny };
}
}
return cell;
}

function createDisjointSet(size: number): Int32Array {
const parent = new Int32Array(size);
for (let i = 0; i < size; i += 1) {
Expand Down
2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('package entry point', () => {
expect(examples.procedural.generateRecursiveMaze).toBe('examples/mazeRecursive.ts');
expect(examples.procedural.generatePrimMaze).toBe('examples/mazePrim.ts');
expect(examples.procedural.generateKruskalMaze).toBe('examples/mazeKruskal.ts');
expect(examples.procedural.generateWilsonMaze).toBe('examples/mazeWilson.ts');
expect(examples.search.Trie).toBe('examples/search.ts');
expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts');
});
Expand Down Expand Up @@ -64,6 +65,7 @@ describe('package entry point', () => {
| 'generateRecursiveMaze'
| 'generatePrimMaze'
| 'generateKruskalMaze'
| 'generateWilsonMaze'
>();
});
});
22 changes: 22 additions & 0 deletions tests/mazeWilson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';

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

describe('generateWilsonMaze', () => {
it('is deterministic per seed', () => {
const options = { width: 21, height: 21, seed: 512 } as const;
const a = generateWilsonMaze(options);
const b = generateWilsonMaze(options);

expect(a.grid).toEqual(b.grid);
expect(a.start).toEqual(b.start);
expect(a.end).toEqual(b.end);
});

it('ensures maze remains bounded by walls', () => {
const { grid } = generateWilsonMaze({ width: 17, height: 17, seed: 11 });
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);
});
});