diff --git a/PROJECT_DESCRIPTION.md b/PROJECT_DESCRIPTION.md index b1a29da..7e4da59 100644 --- a/PROJECT_DESCRIPTION.md +++ b/PROJECT_DESCRIPTION.md @@ -38,7 +38,7 @@ npm run build | 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`, `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`, `createWeightedAliasSampler`, `createObjectPool` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts` | +| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` | | Text & search | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data transforms & diffing | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph traversal | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | diff --git a/README.md b/README.md index e26f7ec..bbd7043 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ CDN usage: | 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`, `createWeightedAliasSampler`, `createObjectPool` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts` | +| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts` | | Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | diff --git a/ROADMAP.md b/ROADMAP.md index 5d11b6d..23cae63 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -48,7 +48,7 @@ - [ ] Delta-time manager for frame-independent timing - [x] Object pool helper for reusable entities - [x] Weighted random selector (alias method) - - [ ] Fisher–Yates shuffle implementation + - [x] Fisher–Yates shuffle implementation - [ ] Bresenham line / raster traversal helpers - Real-time systems: - [ ] 2D camera system (smooth follow, dead zones, screen shake) diff --git a/docs/index.d.ts b/docs/index.d.ts index 2238d51..64b10bd 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -91,6 +91,7 @@ export const examples: { readonly calculateVirtualRange: 'examples/virtualScroll.ts'; readonly createWeightedAliasSampler: 'examples/weightedAlias.ts'; readonly createObjectPool: 'examples/objectPool.ts'; + readonly fisherYatesShuffle: 'examples/fisherYates.ts'; }; readonly ai: { readonly seek: 'examples/steering.ts'; @@ -851,6 +852,13 @@ export interface ObjectPool { */ export function createObjectPool(options: ObjectPoolOptions): ObjectPool; +/** + * Shuffles an array in place using Fisher–Yates. + * Use for: unbiased permutations, testing, random ordering. + * Import: util/fisherYates.ts + */ +export function fisherYatesShuffle(items: T[], options?: { random?: () => number }): T[]; + /** * Least recently used cache. * Use for: memoizing responses, data loaders, pagination caches. diff --git a/examples/fisherYates.ts b/examples/fisherYates.ts new file mode 100644 index 0000000..d5c9309 --- /dev/null +++ b/examples/fisherYates.ts @@ -0,0 +1,7 @@ +import { fisherYatesShuffle } from '../src/index.js'; + +const rng = () => 0.42; +const items = ['a', 'b', 'c', 'd']; +console.log('Original:', items); +fisherYatesShuffle(items, { random: rng }); +console.log('Shuffled:', items); diff --git a/src/index.ts b/src/index.ts index 8e72fcc..2a87cea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,6 +87,7 @@ export const examples = { calculateVirtualRange: 'examples/virtualScroll.ts', createWeightedAliasSampler: 'examples/weightedAlias.ts', createObjectPool: 'examples/objectPool.ts', + fisherYatesShuffle: 'examples/fisherYates.ts', }, ai: { seek: 'examples/steering.ts', @@ -423,6 +424,13 @@ export { createWeightedAliasSampler } from './util/weightedAlias.js'; */ export { createObjectPool } from './util/objectPool.js'; +/** + * Fisher–Yates shuffling utility for unbiased permutations. + * + * Example file: examples/fisherYates.ts + */ +export { fisherYatesShuffle } from './util/fisherYates.js'; + /** * Virtual scroll type exports to help define rendering contracts. */ diff --git a/src/util/fisherYates.ts b/src/util/fisherYates.ts new file mode 100644 index 0000000..b746b24 --- /dev/null +++ b/src/util/fisherYates.ts @@ -0,0 +1,23 @@ +export interface FisherYatesOptions { + random?: () => number; +} + +/** + * Shuffles an array in place using the Fisher–Yates algorithm. + * Useful for: unbiased random permutations for gameplay, sampling, or testing. + */ +export function fisherYatesShuffle(items: T[], options: FisherYatesOptions = {}): T[] { + const { random = Math.random } = options; + if (!Array.isArray(items)) { + throw new Error('items must be an array'); + } + for (let i = items.length - 1; i > 0; i -= 1) { + const r = random(); + if (r < 0 || r >= 1) { + throw new Error('random function must return value in [0,1).'); + } + const j = Math.floor(r * (i + 1)); + [items[i], items[j]] = [items[j], items[i]]; + } + return items; +} diff --git a/tests/fisherYates.test.ts b/tests/fisherYates.test.ts new file mode 100644 index 0000000..c97ef40 --- /dev/null +++ b/tests/fisherYates.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { fisherYatesShuffle } from '../src/index.js'; + +describe('fisherYatesShuffle', () => { + it('shuffles array in place using deterministic random', () => { + const items = [1, 2, 3, 4]; + let index = 0; + const randomValues = [0.1, 0.9, 0.3, 0.7]; + const random = () => { + const value = randomValues[index % randomValues.length] ?? 0; + index += 1; + return value; + }; + + const result = fisherYatesShuffle(items, { random }); + expect(result).toEqual([2, 4, 3, 1]); + expect(result).toBe(items); // in place + }); + + it('throws when random returns value outside [0,1)', () => { + const arr = [1, 2]; + expect(() => fisherYatesShuffle(arr, { random: () => 1 })).toThrow(); + }); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index b8265a4..92880f5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -22,6 +22,7 @@ describe('package entry point', () => { expect(examples.procedural.generateRecursiveDivisionMaze).toBe('examples/mazeDivision.ts'); expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts'); expect(examples.performance.createObjectPool).toBe('examples/objectPool.ts'); + expect(examples.performance.fisherYatesShuffle).toBe('examples/fisherYates.ts'); expect(examples.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts'); expect(examples.search.Trie).toBe('examples/search.ts'); expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts'); @@ -85,6 +86,7 @@ describe('package entry point', () => { | 'calculateVirtualRange' | 'createWeightedAliasSampler' | 'createObjectPool' + | 'fisherYatesShuffle' >(); }); });