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` | `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` |
| 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` |
| 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` | `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` |
| 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` |
| 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/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/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 @@ -41,7 +41,7 @@
- [x] Voronoi diagram helpers for biome/territory generation
- [x] Diamond-square terrain height map generator
- [x] L-system generator for foliage and organic structures
- [ ] Dungeon generation suite (BSP subdivision, rooms & corridors variants)
- [x] Dungeon generation suite (BSP subdivision, rooms & corridors variants)
- [ ] 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
Expand Down
53 changes: 53 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const examples: {
readonly computeVoronoiDiagram: 'examples/voronoi.ts';
readonly diamondSquare: 'examples/diamondSquare.ts';
readonly generateLSystem: 'examples/lSystem.ts';
readonly generateBspDungeon: 'examples/dungeonBsp.ts';
};
readonly spatial: {
readonly Quadtree: 'examples/sat.ts';
Expand Down Expand Up @@ -526,6 +527,58 @@ export interface LSystemResult {
*/
export function generateLSystem(options: LSystemOptions): LSystemResult;

/**
* BSP dungeon generation options.
* Use for: configuring room sizes, recursion depth, deterministic seeds.
* Import: procedural/dungeonBsp.ts
*/
export interface DungeonGeneratorOptions {
width: number;
height: number;
minimumRoomSize?: number;
maximumRoomSize?: number;
maxDepth?: number;
corridorWidth?: number;
seed?: number;
}

/**
* BSP dungeon room description.
* Use for: placing furniture, connecting gameplay triggers.
* Import: procedural/dungeonBsp.ts
*/
export interface DungeonRoom extends Rect {
id: number;
center: Point;
}

/**
* Corridor carved between rooms in a BSP dungeon.
* Import: procedural/dungeonBsp.ts
*/
export interface DungeonCorridor {
path: Point[];
}

/**
* Result returned by the BSP dungeon generator.
* Use for: rendering tiles, analysing connectivity, gameplay logic.
* Import: procedural/dungeonBsp.ts
*/
export interface DungeonBspResult {
grid: number[][];
rooms: DungeonRoom[];
corridors: DungeonCorridor[];
}

/**
* Generates a room-and-corridor dungeon using binary space partitioning.
* Use for: roguelike maps, procedural dungeons, level blocking.
* Performance: O(width × height) carving plus O(nodes) splitting.
* Import: procedural/dungeonBsp.ts
*/
export function generateBspDungeon(options: DungeonGeneratorOptions): DungeonBspResult;

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

const dungeon = generateBspDungeon({
width: 40,
height: 24,
seed: 2024,
minimumRoomSize: 4,
maximumRoomSize: 8,
maxDepth: 4,
});

console.log('Rooms:', dungeon.rooms.length);
console.log('Corridors:', dungeon.corridors.length);
console.log('Sample grid row:', dungeon.grid[12]?.join(''));
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const examples = {
computeVoronoiDiagram: 'examples/voronoi.ts',
diamondSquare: 'examples/diamondSquare.ts',
generateLSystem: 'examples/lSystem.ts',
generateBspDungeon: 'examples/dungeonBsp.ts',
},
spatial: {
Quadtree: 'examples/sat.ts',
Expand Down Expand Up @@ -266,6 +267,13 @@ export { diamondSquare } from './procedural/diamondSquare.js';
*/
export { generateLSystem } from './procedural/lSystem.js';

/**
* Binary space partition dungeon generator.
*
* Example file: examples/dungeonBsp.ts
*/
export { generateBspDungeon } from './procedural/dungeonBsp.js';

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

export interface DungeonGeneratorOptions {
width: number;
height: number;
minimumRoomSize?: number;
maximumRoomSize?: number;
maxDepth?: number;
corridorWidth?: number;
seed?: number;
}

export interface DungeonRoom extends Rect {
id: number;
center: Point;
}

export interface DungeonCorridor {
path: Point[];
}

export interface DungeonBspResult {
grid: number[][];
rooms: DungeonRoom[];
corridors: DungeonCorridor[];
}

interface Node {
bounds: Rect;
depth: number;
left?: Node;
right?: Node;
room?: DungeonRoom;
}

const DEFAULT_MIN_ROOM = 4;
const DEFAULT_MAX_ROOM = 10;
const DEFAULT_MAX_DEPTH = 5;

/**
* Generates a dungeon layout using binary space partitioning (BSP).
* Useful for: roguelike level layouts, room-and-corridor maps.
* Performance: O(width × height) for carving plus O(nodes) splitting.
*/
export function generateBspDungeon({
width,
height,
minimumRoomSize = DEFAULT_MIN_ROOM,
maximumRoomSize = DEFAULT_MAX_ROOM,
maxDepth = DEFAULT_MAX_DEPTH,
corridorWidth = 1,
seed = Date.now(),
}: DungeonGeneratorOptions): DungeonBspResult {
if (width <= 0 || height <= 0) {
throw new Error('width and height must be positive integers.');
}
if (minimumRoomSize < 3) {
throw new Error('minimumRoomSize must be >= 3.');
}
if (maximumRoomSize < minimumRoomSize) {
throw new Error('maximumRoomSize must be >= minimumRoomSize.');
}

const random = createLinearCongruentialGenerator(seed);
const root: Node = {
bounds: { x: 0, y: 0, width, height },
depth: 0,
};

splitNode(root, minimumRoomSize, maxDepth, random);
const rooms: DungeonRoom[] = [];
carveRooms(root, random, minimumRoomSize, maximumRoomSize, rooms);

const corridors: DungeonCorridor[] = [];
connectRooms(root, corridors);

const grid = Array.from({ length: height }, () => Array<number>(width).fill(1));
for (const room of rooms) {
carveRectangle(grid, room, 0);
}
for (const corridor of corridors) {
for (const { x, y } of corridor.path) {
for (let dx = -Math.floor(corridorWidth / 2); dx <= Math.floor(corridorWidth / 2); dx += 1) {
for (let dy = -Math.floor(corridorWidth / 2); dy <= Math.floor(corridorWidth / 2); dy += 1) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
grid[ny][nx] = 0;
}
}
}
}
}

return { grid, rooms, corridors };
}

function splitNode(node: Node, minRoomSize: number, maxDepth: number, random: () => number): void {
if (node.depth >= maxDepth) {
return;
}

const { bounds } = node;
const canSplitHorizontally = bounds.height >= minRoomSize * 2;
const canSplitVertically = bounds.width >= minRoomSize * 2;

if (!canSplitHorizontally && !canSplitVertically) {
return;
}

let splitHorizontally = false;
if (canSplitHorizontally && canSplitVertically) {
splitHorizontally = random() < 0.5;
} else {
splitHorizontally = canSplitHorizontally;
}

if (splitHorizontally) {
const split = randomRange(random, minRoomSize, bounds.height - minRoomSize);
node.left = {
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: split },
depth: node.depth + 1,
};
node.right = {
bounds: {
x: bounds.x,
y: bounds.y + split,
width: bounds.width,
height: bounds.height - split,
},
depth: node.depth + 1,
};
} else {
const split = randomRange(random, minRoomSize, bounds.width - minRoomSize);
node.left = {
bounds: { x: bounds.x, y: bounds.y, width: split, height: bounds.height },
depth: node.depth + 1,
};
node.right = {
bounds: {
x: bounds.x + split,
y: bounds.y,
width: bounds.width - split,
height: bounds.height,
},
depth: node.depth + 1,
};
}

if (node.left) {
splitNode(node.left, minRoomSize, maxDepth, random);
}
if (node.right) {
splitNode(node.right, minRoomSize, maxDepth, random);
}
}

function carveRooms(
node: Node,
random: () => number,
minRoom: number,
maxRoom: number,
rooms: DungeonRoom[],
nextId: { value: number } = { value: 0 }
): void {
if (!node.left && !node.right) {
const maxRoomWidth = Math.min(node.bounds.width - 2, maxRoom);
const maxRoomHeight = Math.min(node.bounds.height - 2, maxRoom);
const roomWidth = randomRange(random, minRoom, maxRoomWidth);
const roomHeight = randomRange(random, minRoom, maxRoomHeight);
const roomX = node.bounds.x + randomRange(random, 1, node.bounds.width - roomWidth - 1);
const roomY = node.bounds.y + randomRange(random, 1, node.bounds.height - roomHeight - 1);
nextId.value += 1;
const room: DungeonRoom = {
id: nextId.value,
x: roomX,
y: roomY,
width: roomWidth,
height: roomHeight,
center: { x: Math.floor(roomX + roomWidth / 2), y: Math.floor(roomY + roomHeight / 2) },
};
node.room = room;
rooms.push(room);
return;
}

if (node.left) {
carveRooms(node.left, random, minRoom, maxRoom, rooms, nextId);
}
if (node.right) {
carveRooms(node.right, random, minRoom, maxRoom, rooms, nextId);
}
}

function connectRooms(node: Node, corridors: DungeonCorridor[]): void {
if (!node.left || !node.right) {
return;
}

const leftRoom = findRoom(node.left);
const rightRoom = findRoom(node.right);
if (leftRoom && rightRoom) {
const path = carveCorridor(leftRoom.center, rightRoom.center);
corridors.push({ path });
}

connectRooms(node.left, corridors);
connectRooms(node.right, corridors);
}

function findRoom(node: Node): DungeonRoom | null {
if (node.room) {
return node.room;
}
const left = node.left ? findRoom(node.left) : null;
if (left) {
return left;
}
return node.right ? findRoom(node.right) : null;
}

function carveCorridor(from: Point, to: Point): Point[] {
const path: Point[] = [];
let x = from.x;
let y = from.y;
while (x !== to.x) {
path.push({ x, y });
x += x < to.x ? 1 : -1;
}
while (y !== to.y) {
path.push({ x, y });
y += y < to.y ? 1 : -1;
}
path.push({ x: to.x, y: to.y });
return path;
}

function carveRectangle(grid: number[][], rect: Rect, value: number): void {
for (let y = rect.y; y < rect.y + rect.height; y += 1) {
for (let x = rect.x; x < rect.x + rect.width; x += 1) {
if (grid[y] && grid[y][x] !== undefined) {
grid[y][x] = value;
}
}
}
}

function randomRange(random: () => number, min: number, max: number): number {
if (max <= min) {
return min;
}
return Math.floor(random() * (max - min + 1)) + min;
}
Loading