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 @@ -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` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts` |
| Web performance & UI throttling | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.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` |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts` |
| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.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` |
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
- [ ] 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)
- [x] Weighted random selector (alias method)
- [ ] Fisher–Yates shuffle implementation
- [ ] Bresenham line / raster traversal helpers
- Real-time systems:
Expand Down
32 changes: 32 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const examples: {
readonly deduplicateRequest: 'examples/requestDedup.ts';
readonly clearRequestDedup: 'examples/requestDedup.ts';
readonly calculateVirtualRange: 'examples/virtualScroll.ts';
readonly createWeightedAliasSampler: 'examples/weightedAlias.ts';
};
readonly ai: {
readonly seek: 'examples/steering.ts';
Expand Down Expand Up @@ -787,6 +788,37 @@ export interface VirtualRange {
}
export function calculateVirtualRange(options: VirtualScrollOptions): VirtualRange;

/**
* Weighted alias sampler entry.
* Use for: pairing values with weights for alias method.
* Import: util/weightedAlias.ts
*/
export interface WeightedAliasEntry<T = string> {
value: T;
weight: number;
}

/**
* Alias sampler descriptor.
* Import: util/weightedAlias.ts
*/
export interface WeightedAliasSampler<T = string> {
sample(random?: () => number): T;
probabilities: number[];
aliases: number[];
values: T[];
}

/**
* Creates a weighted sampler using Vose's alias method.
* Use for: constant-time sampling from discrete weighted distributions.
* Performance: O(n) preprocessing, O(1) sampling.
* Import: util/weightedAlias.ts
*/
export function createWeightedAliasSampler<T>(
entries: ReadonlyArray<WeightedAliasEntry<T>>
): WeightedAliasSampler<T>;

/**
* Least recently used cache.
* Use for: memoizing responses, data loaders, pagination caches.
Expand Down
10 changes: 10 additions & 0 deletions examples/weightedAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createWeightedAliasSampler } from '../src/index.js';

const sampler = createWeightedAliasSampler([
{ value: 'common', weight: 70 },
{ value: 'rare', weight: 25 },
{ value: 'legendary', weight: 5 },
]);

const random = () => 0.42; // deterministic example
console.log('Sampled value:', sampler.sample(random));
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const examples = {
deduplicateRequest: 'examples/requestDedup.ts',
clearRequestDedup: 'examples/requestDedup.ts',
calculateVirtualRange: 'examples/virtualScroll.ts',
createWeightedAliasSampler: 'examples/weightedAlias.ts',
},
ai: {
seek: 'examples/steering.ts',
Expand Down Expand Up @@ -407,6 +408,13 @@ export { deduplicateRequest, clearRequestDedup } from './util/requestDedup.js';
*/
export { calculateVirtualRange } from './util/virtualScroll.js';

/**
* Weighted alias sampler for constant-time discrete sampling.
*
* Example file: examples/weightedAlias.ts
*/
export { createWeightedAliasSampler } from './util/weightedAlias.js';

/**
* Virtual scroll type exports to help define rendering contracts.
*/
Expand Down
86 changes: 86 additions & 0 deletions src/util/weightedAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export interface WeightedAliasEntry<T = string> {
value: T;
weight: number;
}

export interface WeightedAliasSampler<T = string> {
sample(random?: () => number): T;
probabilities: number[];
aliases: number[];
values: T[];
}

/**
* Creates a weighted random sampler using Vose's alias method.
* Useful for: constant-time discrete sampling with many queries.
* Complexity: O(n) preprocessing, O(1) sampling.
*/
export function createWeightedAliasSampler<T>(
entries: ReadonlyArray<WeightedAliasEntry<T>>
): WeightedAliasSampler<T> {
if (entries.length === 0) {
throw new Error('entries must not be empty');
}

let totalWeight = 0;
for (const entry of entries) {
if (entry.weight <= 0) {
throw new Error('weights must be positive numbers');
}
totalWeight += entry.weight;
}
if (totalWeight <= 0) {
throw new Error('Total weight must be greater than zero.');
}

const count = entries.length;
const probabilities = new Array<number>(count);
const aliases = new Array<number>(count).fill(0);
const values = entries.map((entry) => entry.value);

const scaled = entries.map((entry) => (entry.weight * count) / totalWeight);
const small: number[] = [];
const large: number[] = [];

scaled.forEach((value, index) => {
if (value < 1) {
small.push(index);
} else {
large.push(index);
}
});

while (small.length > 0 && large.length > 0) {
const less = small.pop()!;
const more = large.pop()!;

probabilities[less] = scaled[less];
aliases[less] = more;

scaled[more] = scaled[more] + scaled[less] - 1;
if (scaled[more] < 1) {
small.push(more);
} else {
large.push(more);
}
}

for (const remaining of [...small, ...large]) {
probabilities[remaining] = 1;
aliases[remaining] = remaining;
}

function sample(random: () => number = Math.random): T {
const r = random();
if (r < 0 || r >= 1) {
throw new Error('random function must return value in [0,1).');
}
const column = Math.floor(r * count);
const probability = probabilities[column];
const threshold = r * count - column;
const index = threshold < probability ? column : aliases[column];
return values[index];
}

return { sample, probabilities, aliases, values };
}
1 change: 1 addition & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('package entry point', () => {
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.performance.createWeightedAliasSampler).toBe('examples/weightedAlias.ts');
expect(examples.search.Trie).toBe('examples/search.ts');
expect(examples.pathfinding.buildNavMesh).toBe('examples/navMesh.ts');
});
Expand Down
29 changes: 29 additions & 0 deletions tests/weightedAlias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';

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

describe('createWeightedAliasSampler', () => {
it('throws on empty entries', () => {
expect(() => createWeightedAliasSampler([])).toThrow();
});

it('samples according to weights with deterministic random', () => {
const sampler = createWeightedAliasSampler([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 3 },
]);

const rngValues = [0.1, 0.6, 0.8, 0.2, 0.9];
let index = 0;
const random = () => {
const value = rngValues[index % rngValues.length];
index += 1;
return value ?? 0;
};

const samples = Array.from({ length: rngValues.length }, () => sampler.sample(random));
expect(samples.filter((value) => value === 'b').length).toBeGreaterThan(
samples.filter((value) => value === 'a').length
);
});
});