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` | `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` |
| 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` |
| 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` | `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` |
| 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` |
| 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/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/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
9 changes: 9 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const examples: {
readonly generateBspDungeon: 'examples/dungeonBsp.ts';
readonly generateRecursiveMaze: 'examples/mazeRecursive.ts';
readonly generatePrimMaze: 'examples/mazePrim.ts';
readonly generateKruskalMaze: 'examples/mazeKruskal.ts';
};
readonly spatial: {
readonly Quadtree: 'examples/sat.ts';
Expand Down Expand Up @@ -619,6 +620,14 @@ export function generateRecursiveMaze(options: MazeOptions): MazeResult;
*/
export function generatePrimMaze(options: MazeOptions): MazeResult;

/**
* Generates a maze using Kruskal's algorithm with disjoint sets.
* Use for: evenly distributed corridors with minimal bias.
* Performance: O(width × height log cells).
* Import: procedural/maze.ts
*/
export function generateKruskalMaze(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/mazeKruskal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { generateKruskalMaze } from '../src/index.js';

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

console.log('Start:', start);
console.log('End:', end);
console.log('Sample row:', grid[10]?.join(''));
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const examples = {
generateBspDungeon: 'examples/dungeonBsp.ts',
generateRecursiveMaze: 'examples/mazeRecursive.ts',
generatePrimMaze: 'examples/mazePrim.ts',
generateKruskalMaze: 'examples/mazeKruskal.ts',
},
spatial: {
Quadtree: 'examples/sat.ts',
Expand Down Expand Up @@ -290,6 +291,13 @@ export { generateRecursiveMaze } from './procedural/maze.js';
*/
export { generatePrimMaze } from './procedural/maze.js';

/**
* Kruskal's maze generator for evenly distributed layouts.
*
* Example file: examples/mazeKruskal.ts
*/
export { generateKruskalMaze } from './procedural/maze.js';

// ============================================================================
// 🎯 SPATIAL & COLLISION
// ============================================================================
Expand Down
117 changes: 111 additions & 6 deletions src/procedural/maze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ export function generateRecursiveMaze({

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);
carveCorridorBetween(grid, current, next);
carveCell(grid, next);
stack.push(next);
} else {
Expand All @@ -65,7 +61,7 @@ export function generateRecursiveMaze({

/**
* Generates a maze using randomized Prim's algorithm.
* Useful for: maze layouts with different structural characteristics than DFS.
* Useful for: mazes with branching corridors differing from DFS results.
*/
export function generatePrimMaze({
width,
Expand Down Expand Up @@ -99,6 +95,7 @@ export function generatePrimMaze({
if (!neighbour) {
continue;
}

carveCorridorBetween(grid, cell, neighbour);
carveCell(grid, cell);
addFrontierCells(cell, grid, frontier);
Expand All @@ -110,6 +107,70 @@ export function generatePrimMaze({
return { grid, start, end };
}

/**
* Generates a maze using Kruskal's algorithm with a disjoint-set structure.
* Useful for: evenly distributed mazes with minimal bias.
*/
export function generateKruskalMaze({
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 indexMap = new Map<string, number>();
let idx = 0;
for (let y = 1; y < height; y += 2) {
for (let x = 1; x < width; x += 2) {
cells.push({ x, y });
indexMap.set(cellKey(x, y), idx);
idx += 1;
carveCell(grid, { x, y });
}
}

const dsu = createDisjointSet(cells.length);
const edges: Array<{ a: Cell; b: Cell }> = [];

for (const cell of cells) {
const neighbours: Array<[number, number]> = [
[2, 0],
[0, 2],
];
for (const [dx, dy] of neighbours) {
const nx = cell.x + dx;
const ny = cell.y + dy;
if (nx < width && ny < height) {
edges.push({ a: cell, b: { x: nx, y: ny } });
}
}
}

shuffle(edges, random);

for (const edge of edges) {
const aIndex = indexMap.get(cellKey(edge.a.x, edge.a.y));
const bIndex = indexMap.get(cellKey(edge.b.x, edge.b.y));
if (aIndex === undefined || bIndex === undefined) {
continue;
}
if (findSet(dsu, aIndex) !== findSet(dsu, bIndex)) {
unionSet(dsu, aIndex, bIndex);
carveCorridorBetween(grid, edge.a, edge.b);
}
}

const start: Cell = { x: 1, y: 1 };
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 @@ -204,3 +265,47 @@ function findFarthestCell(start: Cell, grid: number[][]): Cell {
}
return farthest;
}

function cellKey(x: number, y: number): string {
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 createDisjointSet(size: number): Int32Array {
const parent = new Int32Array(size);
for (let i = 0; i < size; i += 1) {
parent[i] = -1;
}
return parent;
}

function findSet(parent: Int32Array, index: number): number {
if (parent[index] < 0) {
return index;
}
parent[index] = findSet(parent, parent[index]);
return parent[index];
}

function unionSet(parent: Int32Array, a: number, b: number): void {
const rootA = findSet(parent, a);
const rootB = findSet(parent, b);
if (rootA === rootB) {
return;
}
const sizeA = -parent[rootA];
const sizeB = -parent[rootB];
if (sizeA >= sizeB) {
parent[rootA] -= sizeB;
parent[rootB] = rootA;
} else {
parent[rootB] -= sizeA;
parent[rootA] = rootB;
}
}
2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('package entry point', () => {
expect(examples.procedural.generateBspDungeon).toBe('examples/dungeonBsp.ts');
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.search.Trie).toBe('examples/search.ts');
expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts');
});
Expand Down Expand Up @@ -62,6 +63,7 @@ describe('package entry point', () => {
| 'generateBspDungeon'
| 'generateRecursiveMaze'
| 'generatePrimMaze'
| 'generateKruskalMaze'
>();
});
});
24 changes: 24 additions & 0 deletions tests/mazeKruskal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';

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

describe('generateKruskalMaze', () => {
it('is deterministic for identical seeds', () => {
const options = { width: 21, height: 21, seed: 404 } as const;
const a = generateKruskalMaze(options);
const b = generateKruskalMaze(options);

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

it('produces a maze fully enclosed by walls', () => {
const { grid } = generateKruskalMaze({ width: 17, height: 17, seed: 99 });
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 walkable = grid.flat().filter((cell) => cell === 0).length;
expect(walkable).toBeGreaterThan(0);
});
});