From ec835111932a2ccd1a3c2c45b590c98ee00e47dc Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 7 Oct 2025 21:09:36 +0900 Subject: [PATCH] feat: add recursive division maze generator --- PROJECT_DESCRIPTION.md | 2 +- README.md | 4 +- ROADMAP.md | 2 +- docs/index.d.ts | 10 +++++ examples/mazeDivision.ts | 11 ++++++ src/index.ts | 8 ++++ src/procedural/maze.ts | 77 ++++++++++++++++++++++++++++++++++++++ tests/index.test.ts | 2 + tests/mazeDivision.test.ts | 24 ++++++++++++ 9 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 examples/mazeDivision.ts create mode 100644 tests/mazeDivision.test.ts diff --git a/PROJECT_DESCRIPTION.md b/PROJECT_DESCRIPTION.md index d568f32..fa0d56c 100644 --- a/PROJECT_DESCRIPTION.md +++ b/PROJECT_DESCRIPTION.md @@ -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`, `generateWilsonMaze`, `generateAldousBroderMaze` | `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`, `examples/mazeAldous.ts` | +| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `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`, `examples/mazeAldous.ts`, `examples/mazeDivision.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` | diff --git a/README.md b/README.md index ac1a67e..e12a0ef 100644 --- a/README.md +++ b/README.md @@ -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`, `generateWilsonMaze`, `generateAldousBroderMaze` | `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`, `examples/mazeAldous.ts` | +| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `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`, `examples/mazeAldous.ts`, `examples/mazeDivision.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` | @@ -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/mazeWilson.ts`, `examples/mazeAldous.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/mazeAldous.ts`, `examples/mazeDivision.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. diff --git a/ROADMAP.md b/ROADMAP.md index b45743c..01bee1a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/docs/index.d.ts b/docs/index.d.ts index 718d14b..e9f3644 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -56,6 +56,8 @@ export const examples: { readonly generatePrimMaze: 'examples/mazePrim.ts'; readonly generateKruskalMaze: 'examples/mazeKruskal.ts'; readonly generateWilsonMaze: 'examples/mazeWilson.ts'; + readonly generateAldousBroderMaze: 'examples/mazeAldous.ts'; + readonly generateRecursiveDivisionMaze: 'examples/mazeDivision.ts'; }; readonly spatial: { readonly Quadtree: 'examples/sat.ts'; @@ -645,6 +647,14 @@ export function generateWilsonMaze(options: MazeOptions): MazeResult; */ export function generateAldousBroderMaze(options: MazeOptions): MazeResult; +/** + * Generates a maze using recursive division. + * Use for: structured mazes with nested chambers. + * Performance: O(width × height). + * Import: procedural/maze.ts + */ +export function generateRecursiveDivisionMaze(options: MazeOptions): MazeResult; + /** * Simplex noise generator for smooth gradients without directional artifacts. * Use for: large terrain synthesis, animated textures, volumetric noise. diff --git a/examples/mazeDivision.ts b/examples/mazeDivision.ts new file mode 100644 index 0000000..19c219a --- /dev/null +++ b/examples/mazeDivision.ts @@ -0,0 +1,11 @@ +import { generateRecursiveDivisionMaze } from '../src/index.js'; + +const { grid, start, end } = generateRecursiveDivisionMaze({ + width: 21, + height: 21, + seed: 2025, +}); + +console.log('Start:', start); +console.log('End:', end); +console.log('Sample row:', grid[10]?.join('')); diff --git a/src/index.ts b/src/index.ts index 86435aa..65b5e29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ export const examples = { generateKruskalMaze: 'examples/mazeKruskal.ts', generateWilsonMaze: 'examples/mazeWilson.ts', generateAldousBroderMaze: 'examples/mazeAldous.ts', + generateRecursiveDivisionMaze: 'examples/mazeDivision.ts', }, spatial: { Quadtree: 'examples/sat.ts', @@ -314,6 +315,13 @@ export { generateWilsonMaze } from './procedural/maze.js'; */ export { generateAldousBroderMaze } from './procedural/maze.js'; +/** + * Recursive division maze generator for structured layouts. + * + * Example file: examples/mazeDivision.ts + */ +export { generateRecursiveDivisionMaze } from './procedural/maze.js'; + // ============================================================================ // 🎯 SPATIAL & COLLISION // ============================================================================ diff --git a/src/procedural/maze.ts b/src/procedural/maze.ts index 0c7cce2..02c67c4 100644 --- a/src/procedural/maze.ts +++ b/src/procedural/maze.ts @@ -296,6 +296,40 @@ export function generateAldousBroderMaze({ return { grid, start, end }; } +/** + * Generates a maze using recursive division. + * Useful for: structured mazes with nested chambers and corridors. + */ +export function generateRecursiveDivisionMaze({ + width, + height, + seed = Date.now(), +}: MazeOptions): MazeResult { + validateDimensions(width, height); + + const grid = Array.from({ length: height }, () => Array(width).fill(PATH)); + const random = createLinearCongruentialGenerator(seed); + + // Surround with walls + for (let x = 0; x < width; x += 1) { + grid[0][x] = WALL; + grid[height - 1][x] = WALL; + } + for (let y = 0; y < height; y += 1) { + grid[y][0] = WALL; + grid[y][width - 1] = WALL; + } + + divide(grid, 1, 1, width - 2, height - 2, random); + + const start: Cell = { x: 1, y: 1 }; + 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.'); @@ -460,3 +494,46 @@ function unionSet(parent: Int32Array, a: number, b: number): void { parent[rootA] = rootB; } } + +function divide( + grid: number[][], + x: number, + y: number, + width: number, + height: number, + random: () => number +): void { + if (width < 2 || height < 2) { + return; + } + + const horizontal = width < height ? true : width > height ? false : random() < 0.5; + + if (horizontal) { + const wallY = y + (Math.floor(random() * (height / 2)) * 2 + 1); + const passageX = x + Math.floor(random() * Math.ceil(width / 2)) * 2; + for (let i = x; i < x + width + 1; i += 1) { + if (i === passageX) { + continue; + } + if (grid[wallY] && grid[wallY][i] !== undefined) { + grid[wallY][i] = WALL; + } + } + divide(grid, x, y, width, wallY - y - 1, random); + divide(grid, x, wallY + 1, width, y + height - wallY - 1, random); + } else { + const wallX = x + (Math.floor(random() * (width / 2)) * 2 + 1); + const passageY = y + Math.floor(random() * Math.ceil(height / 2)) * 2; + for (let j = y; j < y + height + 1; j += 1) { + if (j === passageY) { + continue; + } + if (grid[j] && grid[j][wallX] !== undefined) { + grid[j][wallX] = WALL; + } + } + divide(grid, x, y, wallX - x - 1, height, random); + divide(grid, wallX + 1, y, x + width - wallX - 1, height, random); + } +} diff --git a/tests/index.test.ts b/tests/index.test.ts index 51942f6..066e599 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -19,6 +19,7 @@ describe('package entry point', () => { expect(examples.procedural.generateKruskalMaze).toBe('examples/mazeKruskal.ts'); expect(examples.procedural.generateWilsonMaze).toBe('examples/mazeWilson.ts'); expect(examples.procedural.generateAldousBroderMaze).toBe('examples/mazeAldous.ts'); + expect(examples.procedural.generateRecursiveDivisionMaze).toBe('examples/mazeDivision.ts'); expect(examples.search.Trie).toBe('examples/search.ts'); expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts'); }); @@ -68,6 +69,7 @@ describe('package entry point', () => { | 'generateKruskalMaze' | 'generateWilsonMaze' | 'generateAldousBroderMaze' + | 'generateRecursiveDivisionMaze' >(); }); }); diff --git a/tests/mazeDivision.test.ts b/tests/mazeDivision.test.ts new file mode 100644 index 0000000..1838ef3 --- /dev/null +++ b/tests/mazeDivision.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; + +import { generateRecursiveDivisionMaze } from '../src/index.js'; + +describe('generateRecursiveDivisionMaze', () => { + it('is deterministic for identical seeds', () => { + const options = { width: 21, height: 21, seed: 555 } as const; + const a = generateRecursiveDivisionMaze(options); + const b = generateRecursiveDivisionMaze(options); + + expect(a.grid).toEqual(b.grid); + expect(a.start).toEqual(b.start); + expect(a.end).toEqual(b.end); + }); + + it('produces nested chambers bounded by walls', () => { + const { grid } = generateRecursiveDivisionMaze({ width: 17, height: 17, seed: 73 }); + expect(grid.length).toBe(17); + expect(grid.every((row) => row.length === 17)).toBe(true); + 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); + }); +});