From e66e415b6a039657c8249bb0e9356b52c9a4a5ff Mon Sep 17 00:00:00 2001 From: Francois Date: Tue, 7 Oct 2025 15:36:41 +0900 Subject: [PATCH] feat: add wave function collapse synthesiser --- PROJECT_DESCRIPTION.md | 4 +- README.md | 2 +- ROADMAP.md | 105 ++++++-- docs/index.d.ts | 21 ++ docs/list.md | 153 +++++++++++ examples/waveFunctionCollapse.ts | 27 ++ src/index.ts | 1 + src/procedural/waveFunctionCollapse.ts | 354 +++++++++++++++++++++++++ tests/waveFunctionCollapse.test.ts | 68 +++++ 9 files changed, 706 insertions(+), 29 deletions(-) create mode 100644 docs/list.md create mode 100644 examples/waveFunctionCollapse.ts create mode 100644 src/procedural/waveFunctionCollapse.ts create mode 100644 tests/waveFunctionCollapse.test.ts diff --git a/PROJECT_DESCRIPTION.md b/PROJECT_DESCRIPTION.md index 92cc597..beeba8d 100644 --- a/PROJECT_DESCRIPTION.md +++ b/PROJECT_DESCRIPTION.md @@ -35,7 +35,7 @@ npm run build | Need | Algorithm(s) | Module | Example | | ---- | ------------ | ------ | ------- | | Grid pathfinding | `astar`, `dijkstra`, `manhattanDistance`, `gridFromString` | `pathfinding/astar.ts`, `pathfinding/dijkstra.ts` | `examples/astar.ts` | -| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts` | +| Procedural textures & terrain | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.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` | `tests/search.test.ts` | @@ -88,7 +88,7 @@ Consistency between runtime code, documentation, and TypeScript declarations kee ## ✅ Included Implementations (v0.1.0) - **Pathfinding:** A*, Dijkstra, Manhattan heuristic, grid string parser. -- **Procedural:** 2D Perlin grid generator, 3D Perlin sampler. +- **Procedural:** 2D/3D Perlin, Worley noise, Wave Function Collapse tile synthesis. - **Spatial:** Quadtree, AABB helpers, SAT convex polygon collision. - **Performance utilities:** Debounce, throttle, LRU cache, memoize, request deduplication, virtual scrolling. - **Search:** Fuzzy search + scoring, Trie-based autocomplete, binary search, Levenshtein distance. diff --git a/README.md b/README.md index 87594b3..4fccc32 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ CDN usage: | Goal | Algorithms | Import From | Example | | ---- | ---------- | ----------- | ------- | | Pathfinding & navigation | `astar`, `dijkstra`, `manhattanDistance`, `gridFromString` | `pathfinding/astar.ts`, `pathfinding/dijkstra.ts` | `examples/astar.ts` | -| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts` | +| Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.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` | diff --git a/ROADMAP.md b/ROADMAP.md index f1a2a72..33e50d2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,32 +30,85 @@ - [x] Create benchmarking scripts to compare algorithm variants - [x] Expand CI to include coverage gating and bundle size checks -## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned) -- [ ] Implement Wave Function Collapse tile solver with options + example -- [ ] Add dungeon generation suite (BSP subdivision, rooms & corridors variants) -- [ ] Provide L-system generator for foliage/organic structures -- [ ] Ship diamond-square terrain height map generator -- [ ] Offer maze algorithms pack (Kruskal, Wilson, Aldous–Broder, Recursive Division) -- [ ] Add cellular automata cave/organic generator utilities -- [ ] Deliver fixed-timestep game loop utility with interpolation helpers -- [ ] Provide object pool helper for rapid reuse of entities -- [ ] Add weighted random selector (aliasing method) utilities -- [ ] Implement Bresenham line / raster traversal helpers -- [ ] Implement 2D camera system (smooth follow, screen shake, dead zones) -- [ ] Add particle system with configurable emitters -- [ ] Provide sprite animation controller (frame timing, looping, events) -- [ ] Implement platformer physics helper (gravity, coyote time, jump buffering) -- [ ] Ship tile map renderer helpers (chunking, layering) -- [ ] Add shadowcasting field-of-view (FOV) utilities -- [ ] Implement inventory system primitives (stacking, filtering, persistence hooks) -- [ ] Add combat resolution helpers (cooldowns, damage formulas, status effects) -- [ ] Provide quest/dialog state machine utilities -- [ ] Implement 2D lighting helpers (light falloff, blending stubs) -- [ ] Add wave spawner utilities for encounter pacing -- [ ] Provide sound manager stubs (channel limiting, priority) -- [ ] Implement input manager abstraction (key remapping, axis curves) -- [ ] Add save/load serialization helpers (slots, integrity checks) -- [ ] Provide screen transition utilities (fades, wipes, letterboxing) +- ## Milestone 0.4.0 – Procedural Worlds & Game Systems (Planned) +- Procedural generators: + - [x] Wave Function Collapse tile solver with options + example + - [ ] Cellular automata cave/organic generator utilities + - [ ] Poisson disk sampling for even point distribution + - [ ] Voronoi diagram helpers for biome/territory generation + - [ ] Diamond-square terrain height map generator + - [ ] L-system generator for foliage and organic structures + - [ ] 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 + - [ ] Delta-time manager for frame-independent timing + - [ ] Object pool helper for reusable entities + - [ ] Weighted random selector (alias method) + - [ ] Fisher–Yates shuffle implementation + - [ ] Bresenham line / raster traversal helpers +- Real-time systems: + - [ ] 2D camera system (smooth follow, dead zones, screen shake) + - [ ] Particle system with configurable emitters + - [ ] Sprite animation controller (frame timing, events) + - [ ] Tween/lerp utility for smooth interpolation + - [ ] Platformer physics helper (gravity, coyote time, jump buffering) + - [ ] Top-down movement helper (8-direction) + - [ ] Tile map renderer helpers (chunking, layering, collision tags) + - [ ] Shadowcasting field-of-view utilities and minimap helpers +- **Systems for gameplay loops** + - [ ] Inventory system primitives (stacking, filtering, persistence hooks) + - [ ] Combat resolution helpers (cooldowns, damage formulas, status effects) + - [ ] Quest/dialog state machine utilities + - [ ] 2D lighting helpers (falloff, blending stubs) + - [ ] Wave spawner utilities for encounter pacing + - [ ] Sound manager stubs (channel limiting, priority) + - [ ] Input manager abstraction (keyboard/mouse/pad remapping) + - [ ] Save/load serialization helpers (slots, integrity checks) + - [ ] Screen transition utilities (fades, wipes, letterboxing) + +## Milestone 0.5.0 – Algorithm Vault & Data Structures (Planned) +- **AI & behaviour expansions** + - [ ] Finite state machine (FSM) toolkit + - [ ] Genetic algorithm utilities + - [ ] Influence map computation helpers +- **Search & string algorithms** + - [ ] Knuth–Morris–Pratt (KMP) substring search + - [ ] Rabin–Karp multiple pattern matching + - [ ] Boyer–Moore fast substring search + - [ ] Suffix array construction utilities + - [ ] Longest common subsequence (LCS) enhancements and diff helpers +- **Data pipelines & utilities** + - [ ] Flatten/unflatten helpers for nested structures + - [ ] Pagination utilities for client-side paging + - [ ] Advanced diff tooling (tree diff, selective patches) +- **Visual & simulation tools** + - [ ] Color manipulation helpers (RGB/HSL conversion, blending) + - [ ] Force-directed graph layout + - [ ] Marching squares contour extraction + - [ ] Marching cubes isosurface generation +- **Graph algorithms** + - [ ] Minimum spanning tree (Kruskal) + - [ ] Strongly connected components (Tarjan/Kosaraju) + - [ ] Maximum flow (Ford–Fulkerson / Edmonds–Karp) +- **Spatial & collision expansion** + - [ ] Octree partitioning for 3D space + - [ ] Circle collision helpers + - [ ] Raycasting utilities + - [ ] Bounding volume hierarchy (BVH) builder +- **Data structures** + - [ ] Binary heap priority queue + - [ ] Disjoint set union (union-find) + - [ ] Bloom filter probabilistic membership + - [ ] Skip list sorted structure + - [ ] Segment tree range query helper +- **Compression & encoding** + - [ ] Run-length encoding (RLE) + - [ ] Huffman coding utilities + - [ ] LZ77 dictionary compression helper + - [ ] Base64 encode/decode utilities +- **Geometric & numeric utilities** + - [ ] Closest pair of points solver for geometry toolkit ## Milestone 1.0.0 – Production Readiness - [ ] Publish to npm with semver automation and changelog management diff --git a/docs/index.d.ts b/docs/index.d.ts index 2da4e60..f537254 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -99,6 +99,27 @@ export function worleySample( metric?: 'euclidean' | 'manhattan' ): number; +/** + * Wave Function Collapse synthesiser for constraint-based tiles. + * Use for: modular levels, decorative tiling, texture assembly. + * Performance: O(width × height × log tiles) average with retries. + * Import: procedural/waveFunctionCollapse.ts + */ +export interface WfcTile { + id: string; + weight?: number; + rules: Partial>; +} +export interface WaveFunctionCollapseOptions { + width: number; + height: number; + tiles: ReadonlyArray; + seed?: number; + maxRetries?: number; +} +export type WaveFunctionCollapseResult = string[][]; +export function waveFunctionCollapse(options: WaveFunctionCollapseOptions): WaveFunctionCollapseResult; + /** * Simplex noise generator for smooth gradients without directional artifacts. * Use for: large terrain synthesis, animated textures, volumetric noise. diff --git a/docs/list.md b/docs/list.md new file mode 100644 index 0000000..7f795c2 --- /dev/null +++ b/docs/list.md @@ -0,0 +1,153 @@ +🎮 PATHFINDING & NAVIGATION + +A Pathfinding* - Grid/graph shortest path with heuristic +Dijkstra's Algorithm - Shortest path without heuristic +Jump Point Search - A* optimization for uniform grids +Flow Field - Multi-unit pathfinding to same goal +Navigation Mesh - 3D/irregular terrain pathfinding + +🌍 PROCEDURAL GENERATION + +Perlin Noise - Smooth terrain and textures (2D/3D) +Simplex Noise - Improved Perlin alternative +Wave Function Collapse - Constraint-based tile generation +Cellular Automata - Cave and organic structure generation +Poisson Disk Sampling - Even point distribution +Voronoi Diagrams - Territory and region generation +Maze Generation (Recursive Backtrack) - Perfect mazes +Maze Generation (Prim's) - Spanning tree mazes +Maze Generation (Kruskal's) - Random spanning tree +Maze Generation (Wilson's) - Uniform spanning tree +Dungeon Generation (BSP) - Binary space partitioning dungeons +Dungeon Generation (Rooms & Corridors) - Connected room dungeons +L-System - Organic structures, trees, plants, fractals +Diamond-Square Algorithm - Height map terrain generation + +🎯 SPATIAL & COLLISION + +Quadtree - 2D spatial partitioning +Octree - 3D spatial partitioning +AABB Collision - Axis-aligned bounding box detection +SAT Collision - Separating Axis Theorem (polygon collision) +Circle Collision - Round object collision detection +Raycasting - Line of sight and shooting +BVH (Bounding Volume Hierarchy) - Complex 3D collision + +🤖 AI & BEHAVIOR + +Flocking/Boids - Swarm and group movement +Steering Behaviors (Seek) - Move toward target +Steering Behaviors (Flee) - Move away from target +Steering Behaviors (Pursue) - Predict and intercept +Steering Behaviors (Wander) - Random exploration +Steering Behaviors (Arrive) - Slow down at target +Finite State Machine - AI state management +Behavior Trees - Complex AI decision making +Genetic Algorithm - Evolution and optimization +Influence Maps - Tactical AI positioning + +🎲 GAME UTILITIES + +Weighted Random Selection - Probability-based selection +Fisher-Yates Shuffle - Unbiased array randomization +Bresenham's Line - Grid line drawing +Marching Squares - Contour generation from scalar fields +Object Pool - Memory optimization for reusable objects + +🎮 GAME SYSTEMS + +Game Loop - Fixed timestep game loop +Delta Time Manager - Frame-independent timing +Camera 2D - Smooth follow camera with shake +Particle System - Explosions, fire, smoke, effects +Sprite Animation - Frame-based animation controller +Tween/Lerp - Smooth value interpolation +Platformer Physics - Jump, gravity, ground detection +Top-Down Movement - 8-directional movement +Tile Map - Rendering and collision for tile games +Field of View (FOV) - Shadowcasting visibility +Minimap - Radar and overview map +Inventory System - Item management +Combat System - Damage calculation and status effects +Quest/Dialog System - Story progression and missions +Save/Load System - Game state persistence +2D Lighting System - Dynamic lights and shadows +Wave Spawner - Enemy wave management +Sound Manager - Audio playback with pooling +Input Manager - Keyboard, mouse, gamepad handling +Screen Transitions - Fade, slide effects + +⚡ WEB PERFORMANCE + +Debounce - Delay function execution until idle +Throttle - Rate-limit function execution +LRU Cache - Least Recently Used cache +Memoization - Cache function results +Virtual Scrolling - Render only visible list items +Request Deduplication - Prevent duplicate API calls + +🔍 SEARCH & MATCHING + +Fuzzy Search - Approximate string matching +Trie (Prefix Tree) - Fast prefix search +KMP String Search - Efficient substring search +Levenshtein Distance - String edit distance +Binary Search - Sorted array search + +📊 DATA PROCESSING + +Diff Algorithm - Compare arrays/objects for changes +Tree Traversal (BFS) - Breadth-first traversal +Tree Traversal (DFS) - Depth-first traversal +Flatten - Convert nested to flat structure +Unflatten - Convert flat to nested structure +Group By - Organize data by property +Deep Clone - Recursive object copying +Pagination - Client-side data paging + +🎨 VISUAL & ANIMATION + +Easing Functions - Animation curves (20+ types) +Bezier Curves (Quadratic) - Smooth curved paths +Bezier Curves (Cubic) - Complex curved paths +Color Manipulation - RGB/HSL conversion, mixing +Force-Directed Graph - Network visualization layout +Marching Cubes - 3D isosurface from voxels + +📈 GRAPH ALGORITHMS + +Breadth-First Search (BFS) - Level-order traversal +Depth-First Search (DFS) - Deep exploration +Topological Sort - Dependency resolution +Minimum Spanning Tree (Kruskal's) - Network design +Strongly Connected Components - Graph connectivity +Maximum Flow (Ford-Fulkerson) - Network flow + +🔤 STRING ALGORITHMS + +Rabin-Karp - Multiple pattern matching +Boyer-Moore - Fast single pattern search +Longest Common Subsequence - Diff algorithms +Suffix Array - Advanced pattern matching + +🗂️ DATA STRUCTURES + +Binary Heap - Priority queue implementation +Disjoint Set (Union-Find) - Connected components +Bloom Filter - Probabilistic membership testing +Skip List - Sorted data alternative to trees +Segment Tree - Range query operations + +🗜️ COMPRESSION & ENCODING + +Run-Length Encoding (RLE) - Simple compression +Huffman Coding - Optimal prefix compression +LZ77 - Dictionary compression +Base64 - Binary to text encoding + +📐 GEOMETRIC ALGORITHMS + +Convex Hull (Graham Scan) - Boundary point detection +Line Intersection - Check if lines cross +Point in Polygon - Containment testing +Closest Pair of Points - Find nearest points diff --git a/examples/waveFunctionCollapse.ts b/examples/waveFunctionCollapse.ts new file mode 100644 index 0000000..7a92b05 --- /dev/null +++ b/examples/waveFunctionCollapse.ts @@ -0,0 +1,27 @@ +import { waveFunctionCollapse } from '../src/index.js'; + +const tiles = [ + { + id: 'grass', + weight: 3, + rules: { + top: ['grass'], + right: ['grass', 'road'], + bottom: ['grass'], + left: ['grass', 'road'], + }, + }, + { + id: 'road', + weight: 1, + rules: { + top: ['grass', 'road'], + right: ['grass', 'road'], + bottom: ['grass', 'road'], + left: ['grass', 'road'], + }, + }, +]; + +const result = waveFunctionCollapse({ width: 5, height: 5, tiles, seed: 123 }); +console.log(result.map((row) => row.join(' ')).join('\n')); diff --git a/src/index.ts b/src/index.ts index bc40b43..23fb9c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { dijkstra } from './pathfinding/dijkstra.js'; export { perlin, perlin3D } from './procedural/perlin.js'; export { worley, worleySample } from './procedural/worley.js'; export { SimplexNoise, simplex2D, simplex3D } from './procedural/simplex.js'; +export { waveFunctionCollapse } from './procedural/waveFunctionCollapse.js'; export { Quadtree } from './spatial/quadtree.js'; export { aabbCollision, aabbIntersection } from './spatial/aabb.js'; diff --git a/src/procedural/waveFunctionCollapse.ts b/src/procedural/waveFunctionCollapse.ts new file mode 100644 index 0000000..fdcd12d --- /dev/null +++ b/src/procedural/waveFunctionCollapse.ts @@ -0,0 +1,354 @@ +import type { Vector2D } from '../types.js'; + +const EPSILON = 1e-6; + +const DIRECTIONS = [ + { key: 'top', opposite: 'bottom', dx: 0, dy: -1 }, + { key: 'right', opposite: 'left', dx: 1, dy: 0 }, + { key: 'bottom', opposite: 'top', dx: 0, dy: 1 }, + { key: 'left', opposite: 'right', dx: -1, dy: 0 }, +] as const; + +export interface WfcTile { + id: string; + weight?: number; + rules: Partial>; +} + +export interface WaveFunctionCollapseOptions { + width: number; + height: number; + tiles: ReadonlyArray; + seed?: number; + maxRetries?: number; +} + +export type WaveFunctionCollapseResult = string[][]; + +/** + * Wave Function Collapse (WFC) synthesiser for constraint-based tile grids. + * Useful for: modular level layouts, texture assembly, decorative tiling. + * + * @param options - Configuration containing grid size, tile definitions, and optional seed. + * @returns A 2D array of tile identifiers respecting adjacency rules. + * + * @example + * import { waveFunctionCollapse } from 'llm-algorithms'; + * + * const tiles = [ + * { + * id: 'grass', + * weight: 3, + * rules: { + * top: ['grass'], + * right: ['grass', 'road'], + * bottom: ['grass'], + * left: ['grass', 'road'], + * }, + * }, + * { + * id: 'road', + * weight: 1, + * rules: { + * top: ['grass', 'road'], + * right: ['grass', 'road'], + * bottom: ['grass', 'road'], + * left: ['grass', 'road'], + * }, + * }, + * ]; + * + * const grid = waveFunctionCollapse({ width: 4, height: 4, tiles, seed: 42 }); + * console.log(grid.map((row) => row.join(' ')).join('\n')); + * + * @example + * import { waveFunctionCollapse } from 'llm-algorithms'; + * + * const tiles = [ + * { id: 'A', rules: { top: ['A', 'B'], right: ['A'], bottom: ['A', 'B'], left: ['A', 'B'] } }, + * { id: 'B', rules: { top: ['A', 'B'], right: ['B'], bottom: ['A', 'B'], left: ['A', 'B'] } }, + * ]; + * + * const pattern = waveFunctionCollapse({ width: 3, height: 3, tiles, seed: 7 }); + * pattern.forEach((row) => console.log(row)); + */ +export function waveFunctionCollapse(options: WaveFunctionCollapseOptions): WaveFunctionCollapseResult { + validateOptions(options); + const { width, height, tiles, seed = Date.now(), maxRetries = 5 } = options; + + const rng = createRng(seed); + const compat = buildCompatibility(tiles); + + for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + const result = collapse(width, height, tiles, compat, rng.nextUInt32()); + if (result) { + return result; + } + } + + throw new Error('Wave Function Collapse failed to find a solution. Consider relaxing constraints or increasing maxRetries.'); +} + +function collapse( + width: number, + height: number, + tiles: ReadonlyArray, + compat: Compatibility, + seed: number +): WaveFunctionCollapseResult | null { + const rng = createRng(seed); + const cellCount = width * height; + const allTileIndices = tiles.map((_, index) => index); + const possibilities: number[][] = Array.from({ length: cellCount }, () => [...allTileIndices]); + const queue: number[] = []; + + let cellIndex = chooseCellWithLowestEntropy(possibilities, rng); + while (cellIndex !== null) { + const collapsed = collapseCell(cellIndex, possibilities, tiles, rng); + if (!collapsed) { + return null; + } + + queue.push(cellIndex); + while (queue.length > 0) { + const current = queue.shift(); + if (current === undefined) { + continue; + } + const { x, y } = indexToCoord(current, width); + const currentOptions = possibilities[current]; + + for (const dir of DIRECTIONS) { + const nx = x + dir.dx; + const ny = y + dir.dy; + if (nx < 0 || ny < 0 || nx >= width || ny >= height) { + continue; + } + const neighborIndex = coordToIndex(nx, ny, width); + const neighborOptions = possibilities[neighborIndex]; + const allowed = permittedNeighborTiles(currentOptions, compat, dir.key); + const filtered = neighborOptions.filter((tileIndex) => allowed.has(tileIndex)); + if (filtered.length === 0) { + return null; + } + if (filtered.length !== neighborOptions.length) { + possibilities[neighborIndex] = filtered; + queue.push(neighborIndex); + } + } + } + + cellIndex = chooseCellWithLowestEntropy(possibilities, rng); + } + + const grid: string[][] = []; + for (let y = 0; y < height; y += 1) { + const row: string[] = []; + for (let x = 0; x < width; x += 1) { + const options = possibilities[coordToIndex(x, y, width)]; + if (options.length !== 1) { + return null; + } + const tile = tiles[options[0]]; + if (!tile) { + return null; + } + row.push(tile.id); + } + grid.push(row); + } + return grid; +} + +function collapseCell( + cellIndex: number, + possibilities: number[][], + tiles: ReadonlyArray, + rng: Rng +): boolean { + const options = possibilities[cellIndex]; + if (!options) { + return false; + } + if (options.length === 0) { + return false; + } + if (options.length === 1) { + return true; + } + + /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */ + const normalizedWeights: number[] = []; + let totalWeight = 0; + for (let optIndex = 0; optIndex < options.length; optIndex += 1) { + const tileIndex = options[optIndex]; + let normalizedWeight = EPSILON; + if (tileIndex >= 0 && tileIndex < tiles.length) { + const tile = tiles.at(tileIndex); + const weight = tile?.weight ?? 1; + normalizedWeight = weight < EPSILON ? EPSILON : weight; + } + normalizedWeights.push(normalizedWeight); + totalWeight += normalizedWeight; + } + + let threshold = rng.next() * totalWeight; + for (let optIndex = 0; optIndex < options.length; optIndex += 1) { + threshold -= normalizedWeights[optIndex]; + if (threshold <= 0) { + possibilities[cellIndex] = [options[optIndex]]; + /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */ + return true; + } + } + + possibilities[cellIndex] = [options[0]]; + /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */ + return true; +} + +function permittedNeighborTiles(currentOptions: number[], compat: Compatibility, direction: DirectionKey): Set { + const permitted = new Set(); + const neighborSets = + direction === 'top' + ? compat.top + : direction === 'right' + ? compat.right + : direction === 'bottom' + ? compat.bottom + : compat.left; + for (const tileIndex of currentOptions) { + const allowed = neighborSets.at(tileIndex); + if (!allowed) { + continue; + } + for (const neighborIndex of allowed) { + permitted.add(neighborIndex); + } + } + return permitted; +} + +function chooseCellWithLowestEntropy(possibilities: number[][], rng: Rng): number | null { + let bestIndex: number | null = null; + let bestEntropy = Number.POSITIVE_INFINITY; + let ties: number[] = []; + + for (let index = 0; index < possibilities.length; index += 1) { + const count = possibilities[index].length; + if (count <= 1) { + continue; + } + if (count < bestEntropy) { + bestEntropy = count; + bestIndex = index; + ties = [index]; + } else if (count === bestEntropy) { + ties.push(index); + } + } + + if (bestIndex === null) { + return null; + } + if (ties.length > 1) { + return ties[Math.floor(rng.next() * ties.length)]; + } + return bestIndex; +} + +function buildCompatibility(tiles: ReadonlyArray): Compatibility { + const tileCount = tiles.length; + const allIndices = tiles.map((_, index) => index); + const allowAll = new Set(allIndices); + + const compat: Compatibility = { + top: Array.from({ length: tileCount }, () => new Set(allIndices)), + right: Array.from({ length: tileCount }, () => new Set(allIndices)), + bottom: Array.from({ length: tileCount }, () => new Set(allIndices)), + left: Array.from({ length: tileCount }, () => new Set(allIndices)), + }; + + const idToIndex = new Map(); + tiles.forEach((tile, index) => { + idToIndex.set(tile.id, index); + }); + + for (let i = 0; i < tileCount; i += 1) { + const tile = tiles[i]; + if (!tile) { + continue; + } + for (const dir of DIRECTIONS) { + const rule = tile.rules?.[dir.key as keyof typeof tile.rules]; + if (!rule) { + compat[dir.key][i] = new Set(allowAll); + continue; + } + const allowedSet = new Set(); + for (const id of rule) { + const idx = idToIndex.get(id); + if (idx !== undefined) { + allowedSet.add(idx); + } + } + compat[dir.key][i] = allowedSet.size > 0 ? allowedSet : new Set(allowAll); + } + } + + return compat; +} + +function validateOptions(options: WaveFunctionCollapseOptions): void { + if (!Number.isInteger(options.width) || options.width <= 0) { + throw new RangeError('width must be a positive integer'); + } + if (!Number.isInteger(options.height) || options.height <= 0) { + throw new RangeError('height must be a positive integer'); + } + if (!Array.isArray(options.tiles) || options.tiles.length === 0) { + throw new TypeError('tiles must be a non-empty array'); + } +} + +function coordToIndex(x: number, y: number, width: number): number { + return y * width + x; +} + +function indexToCoord(index: number, width: number): Vector2D { + const x = index % width; + const y = Math.floor(index / width); + return { x, y }; +} + +interface Compatibility { + top: Array>; + right: Array>; + bottom: Array>; + left: Array>; +} + +type DirectionKey = keyof Compatibility; + +interface Rng { + next(): number; + nextUInt32(): number; +} + +function createRng(seed: number): Rng { + let state = seed >>> 0; + const random = () => { + state = (state + 0x6d2b79f5) >>> 0; + let t = Math.imul(state ^ (state >>> 15), 1 | state); + t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + return { + next(): number { + return random(); + }, + nextUInt32(): number { + return Math.floor(random() * 0xffffffff); + }, + }; +} diff --git a/tests/waveFunctionCollapse.test.ts b/tests/waveFunctionCollapse.test.ts new file mode 100644 index 0000000..090208e --- /dev/null +++ b/tests/waveFunctionCollapse.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { waveFunctionCollapse } from '../src/procedural/waveFunctionCollapse.js'; +import type { WfcTile } from '../src/procedural/waveFunctionCollapse.js'; + +const tiles: ReadonlyArray = [ + { + id: 'A', + weight: 2, + rules: { + top: ['A', 'B'], + right: ['A', 'B'], + bottom: ['A', 'B'], + left: ['A', 'B'], + }, + }, + { + id: 'B', + weight: 1, + rules: { + top: ['A'], + right: ['A', 'B'], + bottom: ['A'], + left: ['A', 'B'], + }, + }, +] as const satisfies ReadonlyArray<{ + id: string; + weight?: number; + rules: { top?: string[]; right?: string[]; bottom?: string[]; left?: string[] }; +}>; + +describe('waveFunctionCollapse', () => { + it('generates deterministic layout for identical seeds', () => { + const first = waveFunctionCollapse({ width: 3, height: 3, tiles, seed: 7 }); + const second = waveFunctionCollapse({ width: 3, height: 3, tiles, seed: 7 }); + expect(second).toEqual(first); + }); + + it('respects adjacency rules', () => { + const result = waveFunctionCollapse({ width: 3, height: 2, tiles, seed: 13 }); + for (let y = 0; y < result.length; y += 1) { + const row = result[y]; + if (!row) { + continue; + } + for (let x = 0; x < row.length; x += 1) { + const current = row[x]; + if (!current) { + continue; + } + const tile = tiles.find((t) => t.id === current); + if (!tile) { + continue; + } + const right = result[y]?.[x + 1]; + const bottom = result[y + 1]?.[x]; + if (right) { + const allowedRight = tile.rules.right ?? ['A', 'B']; + expect(allowedRight).toContain(right); + } + if (bottom) { + const allowedBottom = tile.rules.bottom ?? ['A', 'B']; + expect(allowedBottom).toContain(bottom); + } + } + } + }); +});