Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
plugins/document-navigator/docs/foundation/random/README.md
Normal file
15
plugins/document-navigator/docs/foundation/random/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# `random`
|
||||
|
||||
Pseudo-random generators, noise functions, and deterministic sequences.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Packages in `random`
|
||||
|
||||
3 packages.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/noise`](./noise/README.md) | Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes… |
|
||||
| [`@suaveplan/pseudo-random`](./pseudo-random/README.md) | Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the `Rng` interface from `@suaveplan/types`. Ships four algorithms — Mulberry32, SF… |
|
||||
| [`@suaveplan/random`](./random/README.md) | High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every… |
|
||||
<!-- end:auto-docs -->
|
||||
@@ -0,0 +1 @@
|
||||
Pseudo-random generators, noise functions, and deterministic sequences.
|
||||
@@ -0,0 +1,66 @@
|
||||
# @suaveplan/noise - Seeded spatial noise fields: Perlin, Simplex, Value, Worley, fBm and Blue-noise sampling for procedural generation
|
||||
|
||||
## Summary
|
||||
|
||||
Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes), fBm (fractal Brownian motion compositor), and Poisson-disk Blue noise sampling — all seeded with `Sfc32Rng` for byte-identical, reproducible output. Every generator is available as both a functional API and a stateful class. Use this package for terrain heightmaps, texture generation, cloud simulation, vegetation scattering, and any procedural system that needs spatially-coherent randomness. For non-spatial random values (UUIDs, shuffles, distributions) use `@suaveplan/random`; for seeded integer/float sequences use `@suaveplan/pseudo-random`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [noise-error](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [utils](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [permutation](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [noise-error](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [permutation](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [noise-error](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [permutation](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [noise-error](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [permutation](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_- [blue](./src/blue/blue.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [noise-error](./src/errors/noise-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_ - [fbm](./src/fbm/fbm.md) — <!-- begin:auto-docs --> _(no submodules)_- [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [perlin](./src/perlin/perlin.md) — <!-- begin:auto-docs --> _(no submodules)_- [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simplex](./src/simplex/simplex.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_ - [permutation](./src/utils/permutation.md) — <!-- begin:auto-docs --> _(no submodules)_- [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_ - [value](./src/value/value.md) — <!-- begin:auto-docs --> _(no submodules)_- [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_ - [worley](./src/worley/worley.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/noise
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Perlin noise** — `perlin2` / `perlin3` (functional) + `PerlinNoise` (class)
|
||||
- **Simplex noise** — `simplex2` / `simplex3` / `simplex4` + `SimplexNoise`
|
||||
- **Value noise** — `value2` / `value3` + `ValueNoise`
|
||||
- **Worley/cellular noise** — `worley2` / `worley3` with `F1`/`F2`/`F2-F1` modes
|
||||
- **fBm compositor** — `fbm2` / `fbm3` wrap any base field into a multi-octave sum
|
||||
- **Blue noise sampler** — `blueNoiseSample` via Bridson's Poisson-disk algorithm
|
||||
- Every field is seeded with `Sfc32Rng` — byte-identical output for equal seeds
|
||||
- Zod schemas for every option bundle
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { PerlinNoise, fbm2, blueNoiseSample } from "@suaveplan/noise";
|
||||
|
||||
const p = new PerlinNoise("terrain");
|
||||
const height = fbm2((x, y) => p.noise2(x, y), 10, 20, { octaves: 6 });
|
||||
const trees = blueNoiseSample({ width: 512, height: 512, radius: 16, seed: 1 });
|
||||
```
|
||||
|
||||
## Subpath exports
|
||||
|
||||
- `@suaveplan/noise/perlin`
|
||||
- `@suaveplan/noise/simplex`
|
||||
- `@suaveplan/noise/value`
|
||||
- `@suaveplan/noise/worley`
|
||||
- `@suaveplan/noise/fbm`
|
||||
- `@suaveplan/noise/blue`
|
||||
- `@suaveplan/noise/errors`
|
||||
|
||||
## Module docs
|
||||
|
||||
See the co-located `.md` next to each implementation.
|
||||
@@ -0,0 +1,53 @@
|
||||
# blue/blue
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `blue` module implements Robert Bridson's fast Poisson-disk sampling algorithm to generate an approximately blue-noise distribution of points in a 2D rectangle. Blue noise's defining property is that every pair of samples is at least `radius` apart, while their positions appear random — it is the gold standard for anti-aliased dithering, stippling, foliage placement, and any sampling application where regular grids produce visible artefacts.
|
||||
|
||||
## Features
|
||||
|
||||
- `blueNoiseSample({ width, height, radius, k?, maxSamples?, seed? })`
|
||||
- Seeded with `Sfc32Rng` for reproducible output
|
||||
- Bridson's spatial grid for O(n) amortised point insertion
|
||||
- `maxSamples` guard converts runaway parameters into a typed `NoiseError` instead of an out-of-memory crash
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { blueNoiseSample } from "@suaveplan/noise";
|
||||
|
||||
const dots = blueNoiseSample({ width: 400, height: 300, radius: 10, seed: 42 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { blueNoiseSample } from "@suaveplan/noise";
|
||||
|
||||
const trees = blueNoiseSample({
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
radius: 32, // minimum canopy spacing
|
||||
k: 50, // higher k = denser packing
|
||||
maxSamples: 20_000,
|
||||
seed: "forest-2026-04-19",
|
||||
});
|
||||
for (const [x, y] of trees) plantTree(x, y);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `BlueNoiseOptions`:
|
||||
- `width`, `height`: rectangle size (must be positive finite)
|
||||
- `radius`: minimum spacing between samples (positive finite)
|
||||
- `k`: candidate neighbours per active sample (default 30)
|
||||
- `maxSamples`: hard ceiling (default 100 000) — throws `NoiseError.BLUE.MAX_SAMPLES_EXCEEDED` if exceeded
|
||||
- `seed`: `string | number` passed through to `Sfc32Rng` (default 0)
|
||||
- `blueNoiseSample(opts): Array<[number, number]>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
A spatial background grid with `cellSize = radius / √2` guarantees at most one sample per cell, so the neighbour check only has to scan a 5×5 window (the 2-cell radius is the Bridson-recommended value). The `maxSamples` guard protects against the common user error of specifying a tiny radius relative to the rectangle — without it, `O(area/radius²)` can explode. `k` above 30 gives denser packing but longer runtimes; 30 is the paper's recommended value.
|
||||
@@ -0,0 +1,65 @@
|
||||
# noise-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `noise-error` module exports the `NoiseError` class and `NOISE_CODES` registry used by every public function in `@suaveplan/noise`. Centralising validation failures here keeps upstream error handling simple: callers only need to match on a single `NOISE.*` code prefix regardless of which noise function raised the error.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable, namespaced error codes via `defineCodes("NOISE", [...])`
|
||||
- `NoiseError` extends `SuaveplanError<NoiseCode>` with the usual cause-chain, redaction, and OTel integrations
|
||||
- Coverage for argument validation, blue-noise termination guards and fBm octave validation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NoiseError, NOISE_CODES } from "@suaveplan/noise/errors";
|
||||
|
||||
throw new NoiseError(
|
||||
NOISE_CODES["ARG.INVALID"],
|
||||
"noise.arg.invalid",
|
||||
{ context: { field: "frequency" } },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NoiseError, NOISE_CODES } from "@suaveplan/noise/errors";
|
||||
|
||||
try {
|
||||
// ... sample blue noise over a huge radius ...
|
||||
} catch (err) {
|
||||
if (err instanceof NoiseError && err.code === NOISE_CODES["BLUE.MAX_SAMPLES_EXCEEDED"]) {
|
||||
// back off and retry with a larger spacing
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NOISE_CODES` — frozen registry.
|
||||
- `NoiseError(code, messageKey, options?)` — structured error with `context`, `cause`, `traceId`, `clock` options.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All codes sit under the `NOISE.*` namespace so they don't collide with `@suaveplan/pseudo-random` or `@suaveplan/random` codes. Adding new codes is strictly additive — removing them requires a major version bump.
|
||||
|
||||
### Subclasses & Codes
|
||||
|
||||
`NoiseError` is the `instanceof` pivot for every error thrown by `@suaveplan/noise`. Concrete subclasses cover invalid seed, invalid octave count, out-of-range frequency, and invalid amplitude. Every subclass carries a stable code from the `NOISE` namespace (`NOISE.INVALID_SEED`, `NOISE.OCTAVES.OUT_OF_RANGE`, etc.) so callers can type-narrow on `error.code` without string-matching the message. Inherits wire serialisation and OTel span-event support from `SuaveplanError`.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NoiseError } from "@suaveplan/noise/errors/noise-error";
|
||||
try {
|
||||
throw new NoiseError("invalid seed", { code: "NOISE.INVALID_SEED" });
|
||||
} catch (e) {
|
||||
if (e instanceof NoiseError) console.log(e.code);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# fbm/fbm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `fbm` module composes Fractional Brownian Motion on top of any base noise function. It layers `octaves` copies of the base at geometrically increasing frequencies (`lacunarity`) and geometrically decreasing amplitudes (`gain`), yielding the classical cloud/terrain/texture multi-scale roughness pattern that underlies most procedurally generated content.
|
||||
|
||||
Because `fbm2`/`fbm3` take the base function as a parameter, you can layer any of `perlin2`, `simplex2`, `value2`, or even Worley distances on top of fBm — the module has no coupling to a specific noise kind.
|
||||
|
||||
## Features
|
||||
|
||||
- `fbm2(base, x, y, opts?)` — 2D compositor
|
||||
- `fbm3(base, x, y, z, opts?)` — 3D compositor
|
||||
- Configurable `octaves`, `lacunarity`, `gain`, `frequency`, `amplitude`
|
||||
- Argument validation via `NoiseError.ARG.INVALID` / `FBM.INVALID_OCTAVES`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fbm2, PerlinNoise } from "@suaveplan/noise";
|
||||
|
||||
const p = new PerlinNoise("world");
|
||||
const h = fbm2((x, y) => p.noise2(x, y), 1.5, 2.3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fbm3, SimplexNoise } from "@suaveplan/noise";
|
||||
|
||||
const s = new SimplexNoise("clouds");
|
||||
const v = fbm3(
|
||||
(x, y, z) => s.noise3(x, y, z),
|
||||
0.1, 0.2, 0.3,
|
||||
{ octaves: 6, lacunarity: 2.0, gain: 0.5, frequency: 1, amplitude: 1 },
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `FbmOptions` — every field optional: `octaves` (default 4, must be an integer 1–32), `lacunarity` (default 2), `gain` (default 0.5, must be in (0, 1]), `frequency` (default 1), `amplitude` (default 1).
|
||||
- `fbm2(base, x, y, opts?): number`
|
||||
- `fbm3(base, x, y, z, opts?): number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Clamping `octaves` to 1–32 avoids pathological input (negative or absurdly large numbers of loop iterations). `gain > 1` would diverge rather than converge, so it's rejected; `gain` exactly 1 means every octave contributes equally, which is useful for certain stylised effects but rejected here to keep the numeric output bounded — if you need that, call the base function manually in a loop.
|
||||
@@ -0,0 +1,50 @@
|
||||
# perlin/perlin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `perlin` module implements Ken Perlin's classic gradient noise in two and three dimensions. Perlin noise is the de-facto standard for procedurally generated terrains, clouds, and texture dithering: it returns a smooth, pseudo-random-looking field in `[-1, 1]` whose value depends continuously on its coordinates.
|
||||
|
||||
This implementation uses the 12-gradient table from Perlin's "Improving Noise" paper (SIGGRAPH 2002), so both 2D and 3D variants share the same gradient space — useful when mixing them in a single shader-like pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- `PerlinNoise` class — holds a seeded permutation table you can reuse across many `noise2`/`noise3` calls
|
||||
- `perlin2(seed, x, y)` / `perlin3(seed, x, y, z)` — one-shot functional convenience
|
||||
- Deterministic per seed; `Sfc32Rng`-shuffled 256-byte permutation table
|
||||
- 2D and 3D outputs in roughly `[-1, 1]` (theoretical max is `√(N/4)` per Perlin, in practice values stay well within `[-0.86, 0.86]` for 2D)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { perlin2 } from "@suaveplan/noise";
|
||||
|
||||
const v = perlin2("terrain-42", 1.5, 2.3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PerlinNoise } from "@suaveplan/noise";
|
||||
|
||||
const p = new PerlinNoise("world");
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
const h = p.noise2(x * 0.05, y * 0.05); // [-1, 1]
|
||||
// ...paint tile height...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PerlinNoise(seed: string | number = 0)` — builds the permutation table once.
|
||||
- `noise2(x: number, y: number): number`
|
||||
- `noise3(x: number, y: number, z: number): number`
|
||||
- `perlin2(seed, x, y): number` / `perlin3(seed, x, y, z): number` — rebuild the table on every call; prefer `PerlinNoise` for hot loops.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Integer coordinates are masked with `& 0xff` — the permutation table is of length 512 with the second 256 entries mirroring the first, so `perm[(perm[xi] + yi) & 0x1ff]` can be written as `perm[perm[xi] + yi]` without risking out-of-bounds reads. Fractional coordinates run through Perlin's quintic fade (`6t^5 - 15t^4 + 10t^3`) for C² continuity.
|
||||
@@ -0,0 +1,54 @@
|
||||
# simplex/simplex
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `simplex` module implements Ken Perlin's Simplex noise (2001) in 2D, 3D, and 4D. Simplex noise is the preferred replacement for classic Perlin noise when you need higher dimensional output: it scales O(N²) instead of O(2^N) in evaluation cost and avoids the axis-aligned anisotropies that plague Perlin noise in 3D+.
|
||||
|
||||
Output is approximately in `[-1, 1]`, after the normalization constants (70 for 2D, 32 for 3D, 27 for 4D) baked into each routine.
|
||||
|
||||
## Features
|
||||
|
||||
- `SimplexNoise` class — reusable seeded permutation table
|
||||
- `simplex2` / `simplex3` / `simplex4` — one-shot functional APIs
|
||||
- Uses the 12-gradient table for 2D/3D and a 32-gradient table for 4D
|
||||
- Deterministic per seed
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { simplex2 } from "@suaveplan/noise";
|
||||
|
||||
const v = simplex2("world", 1.5, 2.3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SimplexNoise } from "@suaveplan/noise";
|
||||
|
||||
const s = new SimplexNoise("clouds");
|
||||
// Animated clouds: use the 4th dimension as time.
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
const v = s.noise4(x * 0.01, y * 0.01, 0, performance.now() * 0.0001);
|
||||
// ... render ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new SimplexNoise(seed)` — constructs the permutation table once.
|
||||
- `noise2(x, y) / noise3(x, y, z) / noise4(x, y, z, w): number`
|
||||
- `simplex2/3/4(seed, ...coords): number` — functional convenience.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 4D simplex-corner selection uses rank-based selection from Gustavson's reference implementation. The `corner{2,3,4}` protected helpers compute the radial falloff — points outside a per-dimension radius contribute zero — then dot with the gradient. Normalisation constants come directly from Perlin's reference code and ensure output stays within `[-1, 1]` for typical inputs.
|
||||
|
||||
### Algorithmic Notes
|
||||
|
||||
The 2D and 3D simplex implementations use the Ken Perlin 2001 reformulation with a 256-entry pre-shuffled permutation table derived deterministically from the supplied seed. Gradient selection uses bitmask indexing into a fixed gradient table so the hot path avoids conditionals. Output range is `[-1, 1]` in all dimensions; callers that want `[0, 1]` should map via `(v + 1) / 2` at the call site.
|
||||
@@ -0,0 +1,53 @@
|
||||
# types/noise.types
|
||||
|
||||
## Purpose
|
||||
|
||||
The `noise.types` module re-exports every public option or mode type used across the `@suaveplan/noise` surface in one flat location. Consumers who only need the types (for instance, library authors publishing their own wrappers on top) can `import type { FbmOptions, WorleyMode, BlueNoiseOptions } from "@suaveplan/noise";` and never pay the runtime cost of loading the implementation modules.
|
||||
|
||||
## Features
|
||||
|
||||
- `FbmOptions` — every field optional (octaves, lacunarity, gain, frequency, amplitude)
|
||||
- `WorleyMode` — `"F1" | "F2" | "F2-F1"`
|
||||
- `BlueNoiseOptions` — rectangle size, radius, k, maxSamples, seed
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { WorleyMode } from "@suaveplan/noise";
|
||||
|
||||
function pickMode(kind: "crack" | "stone"): WorleyMode {
|
||||
return kind === "crack" ? "F2-F1" : "F1";
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { FbmOptions, BlueNoiseOptions } from "@suaveplan/noise";
|
||||
|
||||
interface TerrainRecipe {
|
||||
heightOpts: FbmOptions;
|
||||
treePlacement: BlueNoiseOptions;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the corresponding implementation modules. There is zero runtime code in this file — it exists purely so downstream libraries can consume typings without importing functions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
We keep the flat type barrel deliberately thin: adding more wrappers here invites drift. If a public option type ever gets added to a new noise family, extend this file at the same time as the new module lands.
|
||||
|
||||
### Shapes
|
||||
|
||||
`NoiseFunction2D` and `NoiseFunction3D` are the two canonical call shapes exported by every generator in the package. `NoiseOptions` aggregates seed, octaves, persistence, lacunarity, and amplitude multiplier so fractal / ridged / turbulent wrappers can be composed from a single options bundle. All shapes are type-only and free of runtime cost, consistent with the package-wide `src/types/` convention.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { NoiseFunction2D, NoiseOptions } from "@suaveplan/noise/types/noise.types";
|
||||
const opts: NoiseOptions = { seed: 42, octaves: 4, persistence: 0.5, lacunarity: 2 };
|
||||
declare const fn: NoiseFunction2D;
|
||||
const sample = fn(0.1, 0.2);
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# utils/permutation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `permutation` module provides the seeded 512-byte permutation table plus the `fade` and `lerp` helpers used by every gradient-noise function in `@suaveplan/noise`. Centralising these shared utilities here keeps Perlin, Simplex, and Value noise consistent with one another: they all consume a table shuffled by `Sfc32Rng`.
|
||||
|
||||
## Features
|
||||
|
||||
- `buildPermutation(seed)` — seeded 512-byte Uint8Array; first 256 entries are a Fisher-Yates shuffle of `[0, 255]`, second 256 mirror the first (so hash wrap-around is free)
|
||||
- `fade(t)` — Perlin's quintic fade curve `6t^5 - 15t^4 + 10t^3`
|
||||
- `lerp(a, b, t)` — standard linear interpolation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { buildPermutation, fade } from "@suaveplan/noise";
|
||||
|
||||
const p = buildPermutation("terrain-42");
|
||||
const f = fade(0.5); // 0.5
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { buildPermutation, fade, lerp } from "@suaveplan/noise";
|
||||
|
||||
const perm = buildPermutation(0);
|
||||
// Hash 2D integer coordinates through the table:
|
||||
function hash2(xi: number, yi: number): number {
|
||||
return perm[((perm[xi & 0xff] as number) + (yi & 0xff)) & 0x1ff] as number;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `buildPermutation(seed: string | number): Uint8Array` — deterministic 512-byte table.
|
||||
- `fade(t: number): number` — Perlin's quintic smoothing, returns 0 at t=0 and 1 at t=1.
|
||||
- `lerp(a: number, b: number, t: number): number` — returns `a + (b-a)*t`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Uint8Array uses one byte per entry, so the full table fits in 512 bytes of memory and stays in L1 cache during long noise evaluation loops. The Fisher-Yates shuffle runs 255 steps and uses `rng.nextUint32() % (i+1)` — the small modulo bias is statistically irrelevant for noise-table generation. The second-half mirror lets consumers do `perm[(a + b) & 0x1ff]` without branching or masking `a`/`b` first.
|
||||
@@ -0,0 +1,51 @@
|
||||
# value/value
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `value` module implements 2D and 3D Value noise — the simplest members of the noise family. At every integer lattice point it stores a random value in `[-1, 1]` and smoothly interpolates between them with the quintic fade curve. Value noise is cheaper to evaluate than Perlin noise (no gradient dot-products) but has a visibly axis-aligned structure; it is most useful as an inexpensive layer inside fBm or as a mask for other effects.
|
||||
|
||||
## Features
|
||||
|
||||
- `ValueNoise` class with reusable seeded permutation table
|
||||
- `value2(seed, x, y)` / `value3(seed, x, y, z)` functional APIs
|
||||
- Output normalised to `[-1, 1]`
|
||||
- Deterministic per seed
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { value2 } from "@suaveplan/noise";
|
||||
|
||||
const v = value2("layer", 2.5, 4.1);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ValueNoise } from "@suaveplan/noise";
|
||||
|
||||
const n = new ValueNoise("rocks");
|
||||
let sum = 0;
|
||||
let weight = 1;
|
||||
for (let o = 0; o < 4; o += 1) {
|
||||
sum += weight * n.noise2(1.3 * o, 2.7 * o);
|
||||
weight *= 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new ValueNoise(seed)` — one seeded permutation table, reusable.
|
||||
- `noise2(x, y)` / `noise3(x, y, z)`.
|
||||
- `value2(seed, x, y)` / `value3(seed, x, y, z)` — functional helpers.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The per-lattice-point value is derived from the 256-byte permutation table by reading `perm[(perm[xi] + yi) & 0x1ff]` and rescaling from `[0, 255]` to `[-1, 1]`. Quintic fade keeps the output C² continuous at lattice boundaries, matching the smoothness of our Perlin implementation.
|
||||
|
||||
### Algorithm
|
||||
|
||||
Value noise samples a lattice of pseudo-random values derived from the seed and interpolates with a quintic smoothstep (`6t^5 - 15t^4 + 10t^3`) for continuous first and second derivatives. Cheaper than gradient noise but produces a more block-like visual character — good for low-frequency base layers in a multi-octave stack. The permutation table is identical to the one used by `simplex` so both generators share a single precomputed lookup.
|
||||
@@ -0,0 +1,49 @@
|
||||
# worley/worley
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `worley` module implements Steven Worley's cellular noise in 2D and 3D. At every point in space it computes the distance to the nearest (F1) and second-nearest (F2) feature point, where feature points are scattered per-cell according to a seeded PRNG. The resulting fields (F1, F2, F2-F1) give procedural cell structures used for lava rock, cracked surfaces, Voronoi textures, stone patterns, and biological cell walls.
|
||||
|
||||
## Features
|
||||
|
||||
- `WorleyNoise` class and `worley2` / `worley3` functional APIs
|
||||
- `WorleyMode`: `"F1" | "F2" | "F2-F1"`
|
||||
- 3×3 (2D) / 3×3×3 (3D) neighbourhood search — the canonical compromise between quality and performance
|
||||
- 1..3 feature points per cell, seeded per-cell so different seeds give completely different fields
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { worley2 } from "@suaveplan/noise";
|
||||
|
||||
const distance = worley2("stones", 0.25, 0.75); // F1 default
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { WorleyNoise } from "@suaveplan/noise";
|
||||
|
||||
const w = new WorleyNoise("cracks");
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
const edge = w.noise2(x * 0.05, y * 0.05, "F2-F1");
|
||||
// Paint edges where two cells meet equidistantly.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class WorleyNoise(seed: string | number = 0)` — one seeded field
|
||||
- `noise2(x, y, mode?): number` — `mode` defaults to `"F1"`
|
||||
- `noise3(x, y, z, mode?): number`
|
||||
- `worley2(seed, x, y, mode?): number`
|
||||
- `worley3(seed, x, y, z, mode?): number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Feature points are _not_ cached between evaluations; we recreate the per-cell `Sfc32Rng` each time. This keeps the class stateless and thread-safe but makes repeated evaluation at nearby points slightly more expensive. For heavy workloads the simplest optimisation is to cache `featurePoints2/3` results by cell coordinate — feel free to subclass and override those protected methods.
|
||||
@@ -0,0 +1,75 @@
|
||||
# @suaveplan/pseudo-random - Deterministic seeded PRNG implementations (Mulberry32, SFC32, Xoshiro256++, CounterRng) with offset/fork/checkpoint/restore semantics
|
||||
|
||||
## Summary
|
||||
|
||||
Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the `Rng` interface from `@suaveplan/types`. Ships four algorithms — Mulberry32, SFC32, Xoshiro256++, and CounterRng — with a unified `SeededRng` surface that includes `nextUint32`, `nextFloat`, `skip`, `at`, `fork`, `checkpoint`, and `restore`. Use this package when you need reproducible randomness (procedural generation, game simulations, property tests) or seek O(1) access to arbitrary positions in the sequence. For cryptographically secure randomness use `@suaveplan/random` (CSPRNG-backed) or `@suaveplan/crypto` (raw bytes). Utility functions `shuffle`, `sample`, `choice`, and `weightedChoice` accept any `Rng` and work with both seeded and crypto-backed sources.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [counter-rng](./src/algorithms/counter-rng.md) — `CounterRng` is a hash-based PRNG: its output at logical position `i` is `splitmix64(seed XOR i)`. Because the output d…
|
||||
- [mulberry32](./src/algorithms/mulberry32.md) — `Mulberry32Rng` implements the Mulberry32 PRNG — a compact 32-bit linear-congruential-plus-mix generator with a period…
|
||||
- [sfc32](./src/algorithms/sfc32.md) — `Sfc32Rng` is a 128-bit SFC32 (Small Fast Counting) generator by Chris Doty-Humphrey. It carries four uint32 state word…
|
||||
- [xoshiro256](./src/algorithms/xoshiro256.md) — `Xoshiro256PlusPlusRng` implements Blackman and Vigna's Xoshiro256++ PRNG — 256 bits of state, period 2^256 − 1, with a…
|
||||
- [errors](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [pseudo-random-error](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [seed](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [normalize](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sample](./src/utils/sample.md) — The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` fo…
|
||||
- [shuffle](./src/utils/shuffle.md) — The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-suppli…
|
||||
<!-- end:auto-docs --> - [pseudo-random-error](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [seed](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [normalize](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sample](./src/utils/sample.md) — The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` fo…
|
||||
- [shuffle](./src/utils/shuffle.md) — The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-suppli…
|
||||
- [pseudo-random-error](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [seed](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [normalize](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sample](./src/utils/sample.md) — The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` fo…
|
||||
- [shuffle](./src/utils/shuffle.md) — The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-suppli…
|
||||
- [pseudo-random-error](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [seed](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [normalize](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sample](./src/utils/sample.md) — The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` fo…
|
||||
- [shuffle](./src/utils/shuffle.md) — The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-suppli…
|
||||
- [pseudo-random-error](./src/errors/pseudo-random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [seed](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_ - [normalize](./src/seed/normalize.md) — <!-- begin:auto-docs --> _(no submodules)_- [sample](./src/utils/sample.md) — The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` fo…
|
||||
- [shuffle](./src/utils/shuffle.md) — The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-suppli…
|
||||
|
||||
## Packages
|
||||
|
||||
Four algorithms with identical `SeededRng` surface:
|
||||
|
||||
| Algorithm | State | Period | Best for |
|
||||
| ------------------------- | ------ | ------------- | ------------------------------ |
|
||||
| `Mulberry32Rng` | 32 bit | ~2^32 | Tiny, per-UI seeding |
|
||||
| `Sfc32Rng` | 128bit | ≥2^32 | High-quality default |
|
||||
| `Xoshiro256PlusPlusRng` | 256bit | 2^256 − 1 | Simulations + parallel streams |
|
||||
| `CounterRng` | 64 bit | — | O(1) seek, procedural tiles |
|
||||
|
||||
Each implements `nextUint32`, `nextFloat`, `skip`, `at`, `fork`, `checkpoint`, `restore`, plus `Xoshiro256PlusPlusRng.jump()` for jumping by 2^128 steps.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/pseudo-random
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng, shuffle } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = new Sfc32Rng("game-42");
|
||||
const deck = shuffle(["A", "K", "Q", "J"], rng);
|
||||
```
|
||||
|
||||
## Subpath exports
|
||||
|
||||
- `@suaveplan/pseudo-random/algorithms` — the four generators
|
||||
- `@suaveplan/pseudo-random/seed` — `normalizeSeed`, `splitmix32/64`, `expandSeed`
|
||||
- `@suaveplan/pseudo-random/utils` — `shuffle`, `sample`, `choice`, `weightedChoice`
|
||||
- `@suaveplan/pseudo-random/errors` — `PseudoRandomError`, `PSEUDO_RANDOM_CODES`
|
||||
- `@suaveplan/pseudo-random/schemas` — Zod schemas for serialised state
|
||||
|
||||
## Module docs
|
||||
|
||||
- [algorithms/mulberry32](./src/algorithms/mulberry32.md)
|
||||
- [algorithms/sfc32](./src/algorithms/sfc32.md)
|
||||
- [algorithms/xoshiro256](./src/algorithms/xoshiro256.md)
|
||||
- [algorithms/counter-rng](./src/algorithms/counter-rng.md)
|
||||
- [seed/normalize](./src/seed/normalize.md)
|
||||
- [utils/shuffle](./src/utils/shuffle.md)
|
||||
- [utils/sample](./src/utils/sample.md)
|
||||
- [errors](./src/errors/pseudo-random-error.md)
|
||||
- [types](./src/types/seeded-rng.types.md)
|
||||
@@ -0,0 +1,44 @@
|
||||
# algorithms/counter-rng
|
||||
|
||||
## Purpose
|
||||
|
||||
`CounterRng` is a hash-based PRNG: its output at logical position `i` is `splitmix64(seed XOR i)`. Because the output depends only on position, every seek is constant time — `at(1_000_000)` does not draw a million values, it just advances the counter. This makes `CounterRng` uniquely suited to massively parallel simulations, tile-based procedural generation, and any scenario where you need to sample an arbitrary index of the stream without replaying it.
|
||||
|
||||
## Features
|
||||
|
||||
- O(1) `at(offset)` and `skip(n)` — no per-step iteration cost
|
||||
- 64-bit seed state stored as two uint32 halves for JSON safety
|
||||
- Full `SeededRng` contract
|
||||
- Output quality on par with splitmix64 (passes BigCrush on the upper 32 bits)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CounterRng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = new CounterRng("world");
|
||||
const v = rng.nextUint32();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CounterRng } from "@suaveplan/pseudo-random";
|
||||
|
||||
// Sample tile (1234, 5678) without materialising everything in between.
|
||||
const rng = new CounterRng("map");
|
||||
const tile = rng.at(1234 * 10000 + 5678);
|
||||
const decoration = tile.nextUint32();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new CounterRng(seed)` — accepts the standard `string | number` seed.
|
||||
- `nextUint32()` / `nextFloat()` — one counter step, output is `splitmix64(seed XOR pos).out >> 32`.
|
||||
- `skip(n)` / `at(offset)` — both constant-time.
|
||||
- `fork(id)` — derives a new counter keyed on `(seed, position, id)`.
|
||||
- `checkpoint()` / `restore(cp)` — state is `[seedLo, seedHi]` plus counter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Counter generators expose their index directly, so the usual "position = draws so far" semantics hold regardless of whether you reach position 1000 by calling `nextUint32` 1000 times, calling `skip(1000)`, or jumping via `at(1000)`. The `streamAt` helper is deterministic and stateless, which is what makes the O(1) seek possible.
|
||||
@@ -0,0 +1,51 @@
|
||||
# algorithms/mulberry32
|
||||
|
||||
## Purpose
|
||||
|
||||
`Mulberry32Rng` implements the Mulberry32 PRNG — a compact 32-bit linear-congruential-plus-mix generator with a period of roughly 2^32. It is the smallest `SeededRng` in the package (one 32-bit word of state) and is ideal for UI seedings, per-card shuffles, procedural decorations and any workload where replayability matters but cryptographic strength does not.
|
||||
|
||||
The implementation is byte-identical to the inline generator shipped inside `@suaveplan/testing`'s `seededRng`, so code migrated between the two produces the same draws.
|
||||
|
||||
## Features
|
||||
|
||||
- Single uint32 of state; ~1-cycle nextUint32 cost
|
||||
- Implements the full `SeededRng` contract (`skip`, `at`, `fork`, `checkpoint`, `restore`)
|
||||
- Algorithm-tagged checkpoint prevents cross-algorithm restore bugs
|
||||
- Typed errors on malformed state or invalid arguments
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = new Mulberry32Rng("level-1");
|
||||
console.log(rng.nextFloat());
|
||||
console.log(rng.nextUint32());
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const parent = new Mulberry32Rng("game-42");
|
||||
const encounterRng = parent.fork("encounter");
|
||||
const lootRng = parent.fork("loot");
|
||||
|
||||
const save = parent.checkpoint();
|
||||
// ... simulate many turns ...
|
||||
parent.restore(save); // back to the turn-0 state
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new Mulberry32Rng(seed: string | number)` — normalises the seed through `normalizeSeed`.
|
||||
- `nextUint32()` / `nextFloat()` — uniform draws.
|
||||
- `skip(n)` — advance the stream by `n` uint32 draws; `n` must be a non-negative integer.
|
||||
- `at(offset)` — clone the generator and skip `offset` draws on the clone. Does not mutate `this`.
|
||||
- `fork(id)` — derive a child generator keyed on `(state, position, id)`.
|
||||
- `checkpoint()` / `restore(cp)` — snapshot and reinstate state; restore validates the `algorithm` tag.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The generator advances by `state = (state + 0x6d2b79f5) | 0`, then runs two `Math.imul` mixing rounds before returning the high-entropy output. `at(offset)` is O(offset) because Mulberry32 has no closed-form jump; if you need O(1) seek use `CounterRng` instead. Fork keys include the parent's current position so repeated forks at different moments yield independent streams.
|
||||
@@ -0,0 +1,48 @@
|
||||
# algorithms/sfc32
|
||||
|
||||
## Purpose
|
||||
|
||||
`Sfc32Rng` is a 128-bit SFC32 (Small Fast Counting) generator by Chris Doty-Humphrey. It carries four uint32 state words, runs 12 warmup rounds after seeding, and passes all PractRand tests up to 32TB of output. It is the recommended default `SeededRng` when you want a high-quality stream and don't need an algebraic jump.
|
||||
|
||||
## Features
|
||||
|
||||
- 128-bit state (a,b,c,d) with a counter leg guaranteeing a minimum period of 2^32
|
||||
- 12-round warmup after seed ingestion to decorrelate output from seed bits
|
||||
- Full `SeededRng` contract — `skip`, `at`, `fork`, `checkpoint`, `restore`
|
||||
- Deterministic `fork(id)` keyed on state + position + id
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = new Sfc32Rng("match-7");
|
||||
const first = rng.nextFloat();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const master = new Sfc32Rng("tournament");
|
||||
const player1 = master.fork("p1");
|
||||
const player2 = master.fork("p2");
|
||||
|
||||
const snapshot = master.checkpoint();
|
||||
// ... play out a hand ...
|
||||
master.restore(snapshot);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new Sfc32Rng(seed)` — normalises and expands the seed into 4 × uint32 state words, then applies 12 warmup rounds.
|
||||
- `nextUint32()` / `nextFloat()` — one step of SFC32.
|
||||
- `skip(n)` — O(n) advance.
|
||||
- `at(offset)` — returns a new generator at `position + offset` without mutating `this`.
|
||||
- `fork(id)` — derives an independent child generator keyed on the full state + position + `id`.
|
||||
- `checkpoint()` / `restore(cp)` — snapshot/reinstate, with algorithm-tag validation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The core step is `t = a + b + d; d++; a = b ^ (b >>> 9); b = c + (c << 3); c = rotl(c, 21) + t`. All arithmetic is coerced to uint32 via `| 0` after each operation. State is expanded from the user seed using four splitmix64 taps; this breaks the correlations you would otherwise see between adjacent seeds like `1` and `2`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# algorithms/xoshiro256
|
||||
|
||||
## Purpose
|
||||
|
||||
`Xoshiro256PlusPlusRng` implements Blackman and Vigna's Xoshiro256++ PRNG — 256 bits of state, period 2^256 − 1, with an algebraic `jump()` that advances the stream by 2^128 steps in O(1) without drawing them. It is the top-of-range `SeededRng` in this package: use it when you need parallel non-overlapping streams or when simulation runs will be long enough to exhaust SFC32's 2^32 minimum period.
|
||||
|
||||
## Features
|
||||
|
||||
- 256-bit state (four uint64 words) with a non-zero-state guarantee
|
||||
- `jump()` advances 2^128 draws without intermediate state — ideal for sharding one seed across many workers
|
||||
- Checkpoint serialises state as 8 × uint32 pairs, which round-trip through JSON
|
||||
- Full `SeededRng` contract
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Xoshiro256PlusPlusRng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = new Xoshiro256PlusPlusRng("sim-42");
|
||||
for (let i = 0; i < 1_000_000; i += 1) rng.nextFloat();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Xoshiro256PlusPlusRng } from "@suaveplan/pseudo-random";
|
||||
|
||||
function streamsForWorkers(base: string, n: number) {
|
||||
const rngs: Xoshiro256PlusPlusRng[] = [];
|
||||
let r = new Xoshiro256PlusPlusRng(base);
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
rngs.push(r);
|
||||
const next = new Xoshiro256PlusPlusRng(base);
|
||||
for (let k = 0; k <= i; k += 1) next.jump();
|
||||
r = next;
|
||||
}
|
||||
return rngs;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new Xoshiro256PlusPlusRng(seed)` — splitmix64-expands the seed into 4 × uint64 words.
|
||||
- `nextUint32()` — returns the upper 32 bits of the next uint64 output (better statistical quality than the low bits).
|
||||
- `jump()` — apply the constant-time jump-by-2^128 polynomial.
|
||||
- `skip / at / fork / checkpoint / restore` — `SeededRng` contract as usual.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All 64-bit arithmetic is done with `bigint` masked to 64 bits. Returning the upper 32 bits of each uint64 step is intentional: Xoshiro's low bits have weaker statistical properties than the high bits. The jump polynomial constants are the canonical published values; the loop processes bit-by-bit, XOR-accumulating the current state whenever the jump polynomial bit is set and then advancing a single step.
|
||||
@@ -0,0 +1,64 @@
|
||||
# pseudo-random-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `pseudo-random-error` module defines the stable, code-based error surface for the `@suaveplan/pseudo-random` package. Every failure raised by this package — bad seeds, corrupt checkpoints, invalid arguments to helpers — is reported through a single `PseudoRandomError` class whose `code` is drawn from the frozen `PSEUDO_RANDOM_CODES` registry.
|
||||
|
||||
Centralising error codes gives callers a narrow contract: they can pattern-match on `PSEUDO_RANDOM_CODES["SEED.INVALID"]` instead of string-matching messages, and translators have a finite list of keys to localise.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen, namespaced error-code registry via `defineCodes`
|
||||
- `PseudoRandomError` extends `SuaveplanError` and inherits cause-chain traversal, redaction, OTel mapping and the Zod-validated wire format
|
||||
- Codes cover every validation branch in the package: seed normalisation, state restoration, helper-argument checks
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PseudoRandomError, PSEUDO_RANDOM_CODES } from "@suaveplan/pseudo-random/errors";
|
||||
|
||||
throw new PseudoRandomError(
|
||||
PSEUDO_RANDOM_CODES["SEED.INVALID"],
|
||||
"pseudo_random.seed.invalid",
|
||||
{ context: { value: "???" } },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PseudoRandomError, PSEUDO_RANDOM_CODES } from "@suaveplan/pseudo-random/errors";
|
||||
|
||||
function assertFiniteSeed(seed: number): void {
|
||||
if (!Number.isFinite(seed)) {
|
||||
throw new PseudoRandomError(
|
||||
PSEUDO_RANDOM_CODES["SEED.NOT_FINITE"],
|
||||
"pseudo_random.seed.not_finite",
|
||||
{ context: { seed } },
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PSEUDO_RANDOM_CODES` — the frozen registry of string error codes.
|
||||
- `PseudoRandomError(code, messageKey, options?)` — structured error carrying the code, an i18n key and the standard `SuaveplanErrorOptions` (context/cause/sensitive).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All codes are stable strings (`PSEUDO_RANDOM.*`), suitable for catalog lookup and wire-serialisation. No runtime dependencies beyond `@suaveplan/error`; no OTel emission here — error observability lives in the caller.
|
||||
|
||||
### Subclasses & Codes
|
||||
|
||||
`PseudoRandomError` is the base class for every error thrown by the seeded-RNG implementations. Subclasses cover invalid seed (non-finite, out-of-range), invalid state restore (state vector of wrong length), and exhaustion (fixed-period generators that have cycled past `Number.MAX_SAFE_INTEGER` draws). All codes live under the `PSEUDO_RANDOM` namespace.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PseudoRandomError } from "@suaveplan/pseudo-random/errors/pseudo-random-error";
|
||||
throw new PseudoRandomError("bad seed", { code: "PSEUDO_RANDOM.INVALID_SEED" });
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# seed/normalize
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `normalize` module turns a user-supplied seed (number or string) into a uniformly distributed 32-bit unsigned integer suitable for seeding any of the PRNG algorithms in this package. It also exposes the underlying splitmix32/splitmix64 bit-mixers that each algorithm uses internally for seed expansion.
|
||||
|
||||
Seed normalisation is the only officially supported way to derive a deterministic starting state; passing the same input twice always returns the same uint32, and distinct inputs are decorrelated by the splitmix64 mixing step.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `string | number` seeds; rejects `NaN`, infinities and non-safe integers with a typed `PseudoRandomError`
|
||||
- Deterministic: same input ⇒ same output
|
||||
- Fast 32-bit bit-mixer (`splitmix32`) for scalar mixing
|
||||
- Full 64-bit splitmix state step (`splitmix64`) returning `{ next, out }`
|
||||
- `expandSeed(seed, n)` produces `n` decorrelated uint32 sub-seeds — used by SFC32 (4 words) and Xoshiro256++ (8 words)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { normalizeSeed } from "@suaveplan/pseudo-random/seed";
|
||||
|
||||
const seed = normalizeSeed("my-game-state"); // stable uint32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { expandSeed, splitmix64 } from "@suaveplan/pseudo-random/seed";
|
||||
|
||||
// Derive 4 sub-seeds for SFC32's 128-bit state from a user key:
|
||||
const [a, b, c, d] = expandSeed(normalizeSeed("run-7"), 4);
|
||||
|
||||
// Or drive the raw bigint stream yourself:
|
||||
let state = 1n;
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const { next, out } = splitmix64(state);
|
||||
state = next;
|
||||
console.log(out);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `normalizeSeed(seed: string | number): number` — returns a uint32.
|
||||
- `splitmix32(seed: number): number` — single mix round, returns a uint32.
|
||||
- `splitmix64(state: bigint): { next: bigint; out: bigint }` — one step of the splitmix64 generator, returned unmasked but kept within the low 64 bits.
|
||||
- `expandSeed(seed: number, n: number): Uint32Array` — n decorrelated sub-seeds.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 32-bit mixer is the classic splitmix avalanche. String inputs are folded into a 32-bit FNV-1a accumulator and then run through splitmix32 to destroy any structural correlation in the input alphabet. Number inputs are converted straight to uint32 (via `>>> 0`) before mixing — so seed `-1` and seed `0xFFFFFFFF` are intentionally equivalent. Non-safe integers are rejected because `>>> 0` would silently truncate them and lose user intent.
|
||||
@@ -0,0 +1,59 @@
|
||||
# types/seeded-rng.types
|
||||
|
||||
## Purpose
|
||||
|
||||
The `seeded-rng.types` module declares the `SeededRng` contract every PRNG in this package implements, plus the tagged `RngCheckpoint` shape used by `checkpoint()` and `restore()`. The interface extends `@suaveplan/types`' bare `Rng` with jumping, forking and state-snapshot semantics — exactly the surface needed for replayable simulations, undo/redo, and deterministic game engines.
|
||||
|
||||
## Features
|
||||
|
||||
- `SeededRngAlgorithm` union — stable tag per algorithm (`"mulberry32"`, `"sfc32"`, `"xoshiro256++"`, `"counter"`)
|
||||
- `RngCheckpoint` — opaque, JSON-safe snapshot; algorithm tag guards against cross-algorithm restore
|
||||
- `SeededRng extends Rng` — adds `skip`, `at`, `fork`, `checkpoint`, `restore`, `position`
|
||||
- Zero runtime code — pure type declarations
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { SeededRng } from "@suaveplan/pseudo-random";
|
||||
|
||||
function drawCard(rng: SeededRng, deck: readonly string[]): string {
|
||||
return deck[rng.nextUint32() % deck.length];
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { SeededRng, RngCheckpoint } from "@suaveplan/pseudo-random";
|
||||
|
||||
function replay(rng: SeededRng, cp: RngCheckpoint, steps: number): number[] {
|
||||
rng.restore(cp);
|
||||
const trace: number[] = [];
|
||||
for (let i = 0; i < steps; i += 1) trace.push(rng.nextUint32());
|
||||
return trace;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SeededRngAlgorithm`: `"mulberry32" | "sfc32" | "xoshiro256++" | "counter"`.
|
||||
- `RngCheckpoint`: `{ algorithm; state: readonly number[]; counter: number }`.
|
||||
- `SeededRng`: `Rng` plus `algorithm`, `position`, `skip(n)`, `at(offset)`, `fork(id)`, `checkpoint()`, `restore(cp)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Checkpoints intentionally expose `state` as plain numbers (rather than BigInt) so they round-trip through JSON without custom serialisers. Xoshiro256++ stores its 256-bit state as 8 × uint32 halves; `restore` reassembles the original 64-bit words.
|
||||
|
||||
### Shapes
|
||||
|
||||
`SeededRng` extends the base `Rng` interface from `@suaveplan/types` with `getState` / `setState` methods so deterministic replays can snapshot and restore the internal permutation state. `RngState` is an opaque typed record whose exact shape depends on the concrete algorithm — Mulberry32 stores a single `Uint32Array` of length one, xoshiro256** stores four.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { SeededRng, RngState } from "@suaveplan/pseudo-random/types/seeded-rng.types";
|
||||
declare const rng: SeededRng;
|
||||
const snapshot: RngState = rng.getState();
|
||||
rng.nextUint32();
|
||||
rng.setState(snapshot); // replay from the snapshot
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# utils/sample
|
||||
|
||||
## Purpose
|
||||
|
||||
The `sample` module covers the two sampling primitives that sit above `shuffle`: unique-without-replacement `sample` for drawing `k` items from `n`, and `weightedChoice` for proportional index selection. Both accept any `Rng` and throw typed `PseudoRandomError`s on invalid input.
|
||||
|
||||
These primitives are intentionally small and allocation-frugal: `sample` reuses `shuffle` then slices, so we never allocate auxiliary structures beyond the shuffled copy; `weightedChoice` walks the weights once to compute the total, then once more to find the tipping point.
|
||||
|
||||
## Features
|
||||
|
||||
- `sample(input, count, rng)` — unique draw without replacement
|
||||
- `weightedChoice(weights, rng)` — proportional index draw; rejects negative, NaN or all-zero weights
|
||||
- Both deterministic for equal seeds + equal RNG class
|
||||
- Strict input validation (positive integer counts, finite non-negative weights)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { sample, weightedChoice } from "@suaveplan/pseudo-random/utils";
|
||||
|
||||
const rng = new Mulberry32Rng(1);
|
||||
const hand = sample(["A", "K", "Q", "J", "10"], 3, rng);
|
||||
const bucket = weightedChoice([1, 2, 4], rng); // 0, 1 or 2
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { weightedChoice } from "@suaveplan/pseudo-random/utils";
|
||||
|
||||
type Drop = { id: string; weight: number };
|
||||
const drops: Drop[] = [
|
||||
{ id: "common", weight: 70 },
|
||||
{ id: "rare", weight: 25 },
|
||||
{ id: "epic", weight: 5 },
|
||||
];
|
||||
const rng = new Sfc32Rng("loot-table");
|
||||
const idx = weightedChoice(drops.map((d) => d.weight), rng);
|
||||
const chosen = drops[idx];
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `sample<T>(input, count, rng)` — `PseudoRandomError.ARG.NEGATIVE_COUNT` or `ARG.INVALID` on bad count; `count === 0` returns `[]`.
|
||||
- `weightedChoice(weights, rng)` — `ARG.EMPTY_ARRAY` on empty input; `ARG.WEIGHTS_INVALID` on negative, NaN or all-zero weights.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`weightedChoice` draws `rng.nextFloat()` (a 32-bit uniform in `[0, 1)`), multiplies by the total weight, and walks the cumulative sum. The final fallback (`return weights.length - 1`) guards against floating-point rounding that would otherwise fail to find a bucket when the target sits exactly on the total.
|
||||
@@ -0,0 +1,46 @@
|
||||
# utils/shuffle
|
||||
|
||||
## Purpose
|
||||
|
||||
The `shuffle` module provides deterministic Fisher-Yates shuffling and single-element `choice` drawn from a user-supplied `Rng`. Because `Rng` is the canonical interface defined in `@suaveplan/types`, these utilities compose with every `SeededRng` implementation in this package as well as with the CSPRNG in `@suaveplan/random`.
|
||||
|
||||
Shuffling is a building block for game-card dealing, playlist generation, A/B assignment, and deterministic test data. Isolating it here ensures every downstream package uses the same unbiased Fisher-Yates algorithm rather than re-implementing it (which frequently introduces subtle bias).
|
||||
|
||||
## Features
|
||||
|
||||
- `shuffle(input, rng)` — non-mutating, returns a new shuffled array
|
||||
- `shuffleInPlace(arr, rng)` — mutates the argument, returns it
|
||||
- `choice(input, rng)` — uniformly random element; typed `PseudoRandomError` on empty input
|
||||
- Draws exactly `n - 1` uint32 values per shuffle — counts are predictable for replay
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { shuffle } from "@suaveplan/pseudo-random/utils";
|
||||
|
||||
const rng = new Mulberry32Rng(42);
|
||||
const deck = shuffle(["A", "K", "Q", "J"], rng);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { choice, shuffleInPlace } from "@suaveplan/pseudo-random/utils";
|
||||
|
||||
const rng = new Sfc32Rng("round-12");
|
||||
const deck = ["clubs", "hearts", "diamonds", "spades"];
|
||||
shuffleInPlace(deck, rng);
|
||||
const first = choice(deck, rng);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `shuffle<T>(input: ReadonlyArray<T>, rng: Rng): T[]` — returns a new array.
|
||||
- `shuffleInPlace<T>(arr: T[], rng: Rng): T[]` — mutates and returns.
|
||||
- `choice<T>(input: ReadonlyArray<T>, rng: Rng): T` — throws `PseudoRandomError.ARG.EMPTY_ARRAY` on empty input.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Fisher-Yates loop walks from `n-1` down to `1`, swapping `arr[i]` with `arr[j]` where `j = rng.nextUint32() % (i+1)`. The modulo introduces a negligible bias when `i+1` does not divide 2^32 evenly (max bias ≈ `n / 2^32`); for any `n` short of 2^32 elements this is statistically undetectable and matches the behaviour every other mainstream library ships. If you need provably unbiased sampling for large `n`, use `@suaveplan/random`'s `randomInt`, which re-draws on biased samples.
|
||||
@@ -0,0 +1,109 @@
|
||||
# @suaveplan/random - High-level CSPRNG-backed randomness: strings, UUID/ULID/Nanoid, collections, objects, game primitives, and a full statistical-distribution suite
|
||||
|
||||
## Summary
|
||||
|
||||
High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every common randomness task: unique IDs (`randomUUID` v4, `randomUUIDv7`, `randomULID`, `randomNanoid`), random strings with eight charset presets, collection utilities (`shuffle`, `choice`, `sample`, `weightedChoice`), object utilities (`randomKey`, `randomEntry`, `randomSubset`), game primitives (`coin`, `dice`, `roll`), and a full suite of continuous and discrete statistical distributions (Normal, LogNormal, Binomial, Poisson, and eight more). Every function accepts an optional `rng?: Rng` — omit it for crypto-backed defaults, or pass a `SeededRng` from `@suaveplan/pseudo-random` for deterministic tests. Do not confuse with `@suaveplan/pseudo-random` (seeded, deterministic, for procedural generation) or `@suaveplan/crypto` (raw cryptographic byte generation).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [random-error](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [game](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [dice](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [nanoid](./src/ids/nanoid.md) — The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character a…
|
||||
- [ulid](./src/ids/ulid.md) — The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feer…
|
||||
- [uuid](./src/ids/uuid.md) — The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with…
|
||||
- [random-float](./src/numbers/random-float.md) — The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`.…
|
||||
- [random-int](./src/numbers/random-int.md) — The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.rando…
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [crypto-rng](./src/source/crypto-rng.md) — The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographi…
|
||||
- [default-rng](./src/source/default-rng.md) — The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to wh…
|
||||
- [strings](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [random-string](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-error](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [game](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [dice](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [nanoid](./src/ids/nanoid.md) — The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character a…
|
||||
- [ulid](./src/ids/ulid.md) — The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feer…
|
||||
- [uuid](./src/ids/uuid.md) — The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with…
|
||||
- [random-float](./src/numbers/random-float.md) — The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`.…
|
||||
- [random-int](./src/numbers/random-int.md) — The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.rando…
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-rng](./src/source/crypto-rng.md) — The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographi…
|
||||
- [default-rng](./src/source/default-rng.md) — The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to wh…
|
||||
- [strings](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-string](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-error](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [game](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [dice](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [nanoid](./src/ids/nanoid.md) — The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character a…
|
||||
- [ulid](./src/ids/ulid.md) — The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feer…
|
||||
- [uuid](./src/ids/uuid.md) — The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with…
|
||||
- [random-float](./src/numbers/random-float.md) — The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`.…
|
||||
- [random-int](./src/numbers/random-int.md) — The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.rando…
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-rng](./src/source/crypto-rng.md) — The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographi…
|
||||
- [default-rng](./src/source/default-rng.md) — The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to wh…
|
||||
- [strings](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-string](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-error](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [game](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [dice](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [nanoid](./src/ids/nanoid.md) — The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character a…
|
||||
- [ulid](./src/ids/ulid.md) — The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feer…
|
||||
- [uuid](./src/ids/uuid.md) — The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with…
|
||||
- [random-float](./src/numbers/random-float.md) — The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`.…
|
||||
- [random-int](./src/numbers/random-int.md) — The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.rando…
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-rng](./src/source/crypto-rng.md) — The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographi…
|
||||
- [default-rng](./src/source/default-rng.md) — The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to wh…
|
||||
- [strings](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [random-string](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_- [collections](./src/collections/collections.md) — <!-- begin:auto-docs --> _(no submodules)_- [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_ - [continuous](./src/distributions/continuous/continuous.md) — <!-- begin:auto-docs --> _(no submodules)_- [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_ - [discrete](./src/distributions/discrete/discrete.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sampler](./src/distributions/sampler/sampler.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [random-error](./src/errors/random-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [game](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dice](./src/game/dice.md) — <!-- begin:auto-docs --> _(no submodules)_- [nanoid](./src/ids/nanoid.md) — The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character a…
|
||||
- [ulid](./src/ids/ulid.md) — The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feer…
|
||||
- [uuid](./src/ids/uuid.md) — The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with…
|
||||
- [random-float](./src/numbers/random-float.md) — The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`.…
|
||||
- [random-int](./src/numbers/random-int.md) — The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.rando…
|
||||
- [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_ - [objects](./src/objects/objects.md) — <!-- begin:auto-docs --> _(no submodules)_- [crypto-rng](./src/source/crypto-rng.md) — The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographi…
|
||||
- [default-rng](./src/source/default-rng.md) — The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to wh…
|
||||
- [strings](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_ - [random-string](./src/strings/random-string.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/random
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable default source (`getDefaultRng`/`setDefaultRng`) — defaults to `cryptoRng()`
|
||||
- Unbiased `randomInt` / `randomFloat`
|
||||
- Strings: `randomString` with 8 charset presets + custom alphabets
|
||||
- IDs: `randomUUID` (v4), `randomUUIDv7`, `randomULID`, `randomNanoid`
|
||||
- Collections: `shuffle`, `shuffleInPlace`, `choice`, `sample`, `weightedChoice`, `weightedSample`
|
||||
- Objects: `randomKey`, `randomValue`, `randomEntry`, `randomSubset`
|
||||
- Games: `coin`, `dice`, `roll`
|
||||
- Continuous distributions: Normal, LogNormal, Exponential, Gamma, Beta, Pareto, Weibull, Cauchy, Logistic
|
||||
- Discrete distributions: Bernoulli, Geometric, Binomial, Poisson
|
||||
- Tagged-union `DistributionConfig` with `createSampler` dispatcher — JSON-serialisable
|
||||
|
||||
Every function accepts an optional `rng?: Rng`; omit to use the crypto-backed default, pass a `SeededRng` for deterministic tests.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { randomUUIDv7, shuffle, sampleDistribution } from "@suaveplan/random";
|
||||
|
||||
const id = randomUUIDv7();
|
||||
const deck = shuffle(["A", "K", "Q", "J"]);
|
||||
const draws = Array.from({ length: 1000 }, () =>
|
||||
sampleDistribution({ type: "normal", mean: 0, stddev: 1 })
|
||||
);
|
||||
```
|
||||
|
||||
## Subpath exports
|
||||
|
||||
- `@suaveplan/random/source` — `cryptoRng`, `getDefaultRng`, `setDefaultRng`, `randomBytes`
|
||||
- `@suaveplan/random/strings` — `randomString`, `CHARSETS`
|
||||
- `@suaveplan/random/ids` — `randomUUID`, `randomUUIDv7`, `randomULID`, `randomNanoid`
|
||||
- `@suaveplan/random/collections` — `shuffle`, `choice`, `sample`, `weightedChoice`, …
|
||||
- `@suaveplan/random/objects` — `randomKey`, `randomValue`, `randomEntry`, `randomSubset`
|
||||
- `@suaveplan/random/game` — `coin`, `dice`, `roll`
|
||||
- `@suaveplan/random/numbers` — `randomInt`, `randomFloat`
|
||||
- `@suaveplan/random/distributions` — all distributions + `DistributionConfig`/`createSampler`
|
||||
- `@suaveplan/random/errors` — `RandomError`, `RANDOM_CODES`
|
||||
|
||||
## Module docs
|
||||
|
||||
See the co-located `.md` next to each implementation.
|
||||
@@ -0,0 +1,54 @@
|
||||
# collections/collections
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `collections` module re-presents the five core array-shaped random helpers — `shuffle`, `shuffleInPlace`, `choice`, `sample`, `weightedChoice`, `weightedSample` — with the same ergonomic ‘optional `rng?`' signature used across `@suaveplan/random`. Internally each function delegates to `@suaveplan/pseudo-random`'s unbiased implementation, so you pay no duplication tax but gain the CSPRNG-by-default convenience.
|
||||
|
||||
## Features
|
||||
|
||||
- `shuffle` / `shuveInPlace` — Fisher-Yates, preserves multiset
|
||||
- `choice` — single uniform draw
|
||||
- `sample` — unique, without replacement
|
||||
- `weightedChoice(items, weight, rng?)` — selects an item via a weight function
|
||||
- `weightedSample(items, weight, count, rng?)` — unique-without-replacement weighted draw
|
||||
- Every helper works with no `rng?` argument (uses `getDefaultRng()`)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { shuffle, choice, sample } from "@suaveplan/random/collections";
|
||||
|
||||
const deck = shuffle(["A", "K", "Q", "J"]);
|
||||
const top = choice(deck);
|
||||
const three = sample(deck, 3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { weightedSample } from "@suaveplan/random/collections";
|
||||
|
||||
const items = [
|
||||
{ id: "common", rarity: 70 },
|
||||
{ id: "rare", rarity: 25 },
|
||||
{ id: "epic", rarity: 5 },
|
||||
];
|
||||
const loot = weightedSample(items, (i) => i.rarity, 2, new Sfc32Rng("seed"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `shuffle<T>(input, rng?): T[]` — new array.
|
||||
- `shuffleInPlace<T>(arr, rng?): T[]` — mutates + returns.
|
||||
- `choice<T>(input, rng?): T` — throws `PseudoRandomError.ARG.EMPTY_ARRAY` on empty input.
|
||||
- `sample<T>(input, count, rng?): T[]` — throws on invalid count.
|
||||
- `weightedChoice<T>(items, weight, rng?): T` — throws `RandomError.ARG.EMPTY` on empty.
|
||||
- `weightedSample<T>(items, weight, count, rng?): T[]` — removes picks from the pool so every returned element is unique.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`weightedSample` maintains parallel `pool` and `weights` arrays and splices out the chosen index each round. For `n` items and `k` draws the cost is O(n·k); there is a theoretically faster reservoir algorithm (Efraimidis–Spirakis) but it is rarely worth the extra complexity at the scales this library targets.
|
||||
@@ -0,0 +1,51 @@
|
||||
# distributions/continuous
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `continuous` module implements the nine continuous probability distributions most commonly used in simulations, analytics, and game mechanics: Normal (Gaussian), LogNormal, Exponential, Gamma, Beta, Pareto, Weibull, Cauchy and Logistic. Each is a stateless function taking parameters and an optional `rng?`; together they cover the overwhelming majority of non-uniform sampling needs.
|
||||
|
||||
## Features
|
||||
|
||||
- `normal(mean, stddev, rng?)` — Box-Muller transform
|
||||
- `logNormal(mu, sigma, rng?)` — exponential of a Normal
|
||||
- `exponential(lambda, rng?)` — inverse-CDF method
|
||||
- `gamma(shape, scale, rng?)` — Marsaglia–Tsang for α ≥ 1, Ahrens-Dieter boost for α < 1
|
||||
- `beta(alpha, beta, rng?)` — two Gamma draws
|
||||
- `pareto(xm, alpha, rng?)` — inverse-CDF
|
||||
- `weibull(k, lambda, rng?)` — inverse-CDF
|
||||
- `cauchy(x0, gamma, rng?)` — tangent-transform
|
||||
- `logistic(mu, s, rng?)` — inverse-CDF
|
||||
- Every function validates positivity of scale parameters and throws `RandomError.DISTRIBUTION.INVALID_PARAM`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { normal, exponential } from "@suaveplan/random/distributions";
|
||||
|
||||
const z = normal(0, 1);
|
||||
const wait = exponential(2);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { gamma, beta } from "@suaveplan/random/distributions";
|
||||
|
||||
const rng = new Sfc32Rng("mc-sim-42");
|
||||
const serviceTime = gamma(2, 3, rng);
|
||||
const conversionRate = beta(8, 92, rng); // ~8% with informative prior
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions: `(...params, rng?): number`. Parameter positivity is validated; violations raise `RandomError.DISTRIBUTION.INVALID_PARAM`. Default parameter values match the standard form of each distribution (e.g. `normal()` returns a standard Normal).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Box-Muller discards the paired draw; this is the simpler variant of the transform and has no measurable performance cost at JS call scales.
|
||||
- The Marsaglia–Tsang Gamma sampler is the fastest acceptance-rejection method known for α ≥ 1. For α < 1 we use the boost trick `gamma(α) = gamma(α+1) · U^(1/α)`.
|
||||
- Beta uses two Gamma draws from the same source, which leaves the mean and variance exactly right without requiring a specialised sampler.
|
||||
@@ -0,0 +1,48 @@
|
||||
# distributions/discrete
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `discrete` module implements the four most commonly used discrete distributions: Bernoulli, Geometric, Binomial, and Poisson. Each is a small standalone function taking parameters and an optional `rng?`. Together they cover coin flips, first-success modelling, fixed-trial counting, and rare-event counting — the core of most statistical simulations and agent-based models.
|
||||
|
||||
## Features
|
||||
|
||||
- `bernoulli(p)` — 0/1 with probability `p`
|
||||
- `geometric(p)` — number of failures before first success
|
||||
- `binomial(n, p)` — direct Bernoulli-sum method (O(n))
|
||||
- `poisson(lambda)` — Knuth's multiplicative method for λ ≤ 30; Atkinson's rejection method for larger λ
|
||||
- All parameters are validated (range, integrality); bad inputs throw `RandomError.DISTRIBUTION.INVALID_PARAM`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { bernoulli, poisson } from "@suaveplan/random/distributions";
|
||||
|
||||
const click = bernoulli(0.02);
|
||||
const arrivals = poisson(4);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { binomial } from "@suaveplan/random/distributions";
|
||||
|
||||
const rng = new Sfc32Rng("ab-test");
|
||||
const sample = binomial(1000, 0.08, rng); // expected ~80
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `bernoulli(p, rng?): number`
|
||||
- `geometric(p, rng?): number`
|
||||
- `binomial(n, p, rng?): number`
|
||||
- `poisson(lambda, rng?): number`
|
||||
|
||||
All validate inputs (0 ≤ p ≤ 1, n a non-negative integer, λ > 0).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Knuth's Poisson algorithm is numerically stable up to λ ≈ 30 but gets slow and loses precision beyond that; crossing the threshold to Atkinson's rejection method maintains throughput for heavy-tailed rates. `binomial` uses the simple sum-of-Bernoullis method, which is O(n) per call — fine for `n ≤ 100k` in practice. For very large n with very small p, prefer `poisson(n·p)` as a fast approximation.
|
||||
@@ -0,0 +1,51 @@
|
||||
# distributions/sampler
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `sampler` module turns the eighteen distribution samplers in this package into a single data-driven surface: a tagged-union `DistributionConfig` plus the `sample(config, rng?)` dispatcher. It is the piece that makes distribution choice serialisable — configs can be persisted, A/B-tested, or passed over the wire as JSON and then reconstituted via the Zod schema.
|
||||
|
||||
## Features
|
||||
|
||||
- `DistributionConfig` — tagged union covering all nine continuous + four discrete distributions
|
||||
- `DistributionConfigSchema` — Zod schema with discriminated union, sensible defaults, positivity constraints
|
||||
- `sample(config, rng?)` — dispatch to the right underlying sampler
|
||||
- `createSampler(config, rng?): () => number` — zero-arg closure for hot loops
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sample } from "@suaveplan/random/distributions";
|
||||
|
||||
const arrival = sample({ type: "poisson", lambda: 4 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DistributionConfigSchema, createSampler } from "@suaveplan/random/distributions";
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
|
||||
const raw = '{"type":"gamma","shape":2,"scale":3}';
|
||||
const cfg = DistributionConfigSchema.parse(JSON.parse(raw));
|
||||
const rng = new Sfc32Rng("sim-2026-04-19");
|
||||
const draw = createSampler(cfg, rng);
|
||||
const batch = Array.from({ length: 10_000 }, () => draw());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DistributionConfig` — `{ type: "normal" | "logNormal" | ... ; ...params }`.
|
||||
- `DistributionConfigSchema` — Zod discriminated union.
|
||||
- `sample(config, rng?): number` — unknown `type` throws `RandomError.DISTRIBUTION.UNKNOWN_TYPE`.
|
||||
- `createSampler(config, rng?): () => number`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Default values in the schema match the standard-form parameters of each distribution (mean 0, scale 1, etc). Zod's `.default()` values are applied during parsing, so if a config omits optional fields they fill in without downstream code having to care. Having a Zod schema also lets callers validate user-supplied JSON without writing bespoke type guards.
|
||||
|
||||
### Distributions
|
||||
|
||||
The sampler family covers uniform (`[a, b)`), normal (Box-Muller transform), exponential (inverse CDF), and weighted-discrete via alias tables for O(1) draws over large categorical distributions. Every sampler takes an injected `Rng` so tests can pin draws against golden outputs. The alias-table constructor runs in O(n) amortised time, making it suitable for distribution rebuilds inside hot loops.
|
||||
@@ -0,0 +1,63 @@
|
||||
# random-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `random-error` module exports the stable error-code registry and typed `RandomError` class used by every public function in `@suaveplan/random`. Every failure — missing CSPRNG, out-of-range argument, malformed distribution parameters, invalid charset preset — reports through the same channel so callers only ever have to switch on one error code namespace.
|
||||
|
||||
Grouping errors by a code prefix (`RANDOM.SOURCE`, `RANDOM.ARG`, `RANDOM.STRING`, `RANDOM.DISTRIBUTION`) keeps messages localisable and programmatically filterable.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable, frozen code registry via `defineCodes("RANDOM", [...])`
|
||||
- `RandomError` extends `SuaveplanError<RandomCode>` — full cause-chain, redaction, OTel-span integration
|
||||
- Coverage across source availability, argument validation, string generation and distribution validation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RandomError, RANDOM_CODES } from "@suaveplan/random/errors";
|
||||
|
||||
throw new RandomError(
|
||||
RANDOM_CODES["ARG.OUT_OF_RANGE"],
|
||||
"random.int.out_of_range",
|
||||
{ context: { min: 10, max: 5 } },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RandomError, RANDOM_CODES } from "@suaveplan/random/errors";
|
||||
|
||||
try {
|
||||
// ... call into @suaveplan/random ...
|
||||
} catch (err) {
|
||||
if (err instanceof RandomError && err.code === RANDOM_CODES["SOURCE.UNAVAILABLE"]) {
|
||||
// fall back to a SeededRng for test environments
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RANDOM_CODES` — frozen record of all error codes.
|
||||
- `RandomError(code, messageKey, options?)` — structured error carrying context, cause, traceId and clock.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Error codes intentionally avoid stuttering with the package name: a consumer writing `RANDOM_CODES["ARG.INVALID"]` gets `"RANDOM.ARG.INVALID"` on the wire. Adding codes is a strictly additive change; removing them is a breaking one.
|
||||
|
||||
### Subclasses & Codes
|
||||
|
||||
`RandomError` is the package-level base class. Subclasses cover invalid range (`min >= max`), invalid length for `randomBytes`, and weight-vector errors in the weighted-choice helper (empty vector, negative weights, NaN). Codes live under the `RANDOM` namespace.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RandomError } from "@suaveplan/random/errors/random-error";
|
||||
throw new RandomError("bad range", { code: "RANDOM.RANGE.INVALID" });
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# game/dice
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `dice` module provides three small but ubiquitous primitives for game and simulation code: `coin()`, `dice(sides)`, and `roll(count, sides)`. They all build on `randomInt` so draws are unbiased; they all accept an optional `rng?` so tests can make rolls deterministic.
|
||||
|
||||
Packaging these trivially-implementable functions here keeps the game packages (rummy, bingo, baccarat, minesweeper, etc.) uniform and eliminates the temptation to sprinkle raw `Math.floor(Math.random() * …)` calls through production code.
|
||||
|
||||
## Features
|
||||
|
||||
- `coin` — fair coin flip with string results (`"heads"` or `"tails"`)
|
||||
- `dice` — single-die roll; validates `sides >= 1`
|
||||
- `roll` — multi-die roll returning the vector of individual results
|
||||
- Optional `rng?` on every function
|
||||
- Unbiased integer draws via `randomInt`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { coin, dice, roll } from "@suaveplan/random/game";
|
||||
|
||||
const flip = coin(); // "heads" | "tails"
|
||||
const d20 = dice(20); // integer in [1, 20]
|
||||
const hand = roll(5, 6); // five six-sided dice
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { roll } from "@suaveplan/random/game";
|
||||
|
||||
const rng = new Sfc32Rng("tournament-round-3");
|
||||
const damage = roll(3, 6, rng).reduce((a, b) => a + b, 0); // 3d6
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `coin(rng?): "heads" | "tails"`
|
||||
- `dice(sides, rng?): number` — throws `RandomError.ARG.OUT_OF_RANGE` on `sides < 1` or non-integer `sides`.
|
||||
- `roll(count, sides, rng?): number[]` — throws on negative/non-integer counts; `count === 0` returns `[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`coin` is implemented via `randomInt(0, 1)` rather than `nextUint32() & 1` so the underlying rejection sampler handles any biases even though the range is a power of two. The minor overhead is irrelevant for the typical hundreds-of-calls-per-turn workload.
|
||||
|
||||
### Notation
|
||||
|
||||
`rollDice` accepts the classic tabletop notation `NdS+M` where N is the number of dice, S is the side count, and M is a fixed modifier. The parser handles `d20`, `2d6+3`, and `4d6` drop-lowest via the explicit `rollDrop` helper. Each individual roll is a fresh draw from the injected `Rng`, and the return value carries both the per-die results and the final total so tabletop UIs can render the breakdown.
|
||||
@@ -0,0 +1,43 @@
|
||||
# ids/nanoid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `nanoid` module implements the Nano ID scheme: a configurable-length, URL-safe random string using a 64-character alphabet. The default 21-character output encodes ~126 bits of entropy — comparable to UUID — in a substantially shorter, more human-friendly form.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomNanoid(length?, alphabet?, rng?)` — every parameter is optional
|
||||
- Default 21-character, 64-char alphabet matches the canonical `nanoid` JS library byte-for-byte
|
||||
- Unbiased character draws via `randomInt` rejection sampling
|
||||
- Typed errors on negative length or empty alphabet
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomNanoid } from "@suaveplan/random/ids";
|
||||
|
||||
const id = randomNanoid(); // 21 chars
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomNanoid } from "@suaveplan/random/ids";
|
||||
|
||||
const shortCode = randomNanoid(6, "ABCDEFGHJKMNPQRSTUVWXYZ23456789", new Mulberry32Rng("promo"));
|
||||
// 6 characters; no ambiguous 0/O/I/1/l
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NANOID_ALPHABET` — the canonical 64-character Nano ID alphabet.
|
||||
- `randomNanoid(length = 21, alphabet = NANOID_ALPHABET, rng?): string` — throws `RandomError.ARG.INVALID` on bad length or `STRING.EMPTY_CHARSET` on empty alphabet.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The reference Nano ID implementation uses a bitmask rejection loop rather than modulo rejection; here we reuse `randomInt`, which achieves the same uniformity guarantee through a slightly different mechanism. Output is byte-for-byte comparable across equal seeds when both approaches consume the same number of uint32 draws per character — which is true for alphabets whose length divides 2^32, and only slightly different (at the rejection boundary) otherwise.
|
||||
|
||||
### ID Properties
|
||||
|
||||
The default 21-character nanoid uses the 64-entry URL-safe alphabet `A–Z a–z 0–9 _ -`, giving ~126 bits of entropy — collision-resistant for any realistic workload. The alphabet and length are both overrideable, and the RNG is injected rather than sourced globally so tests can pin outputs. Internally it draws uniform `Uint32` values and rejects bytes that would bias the distribution modulo alphabet size.
|
||||
@@ -0,0 +1,40 @@
|
||||
# ids/ulid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `ulid` module implements the Universally Unique Lexicographically Sortable Identifier per the original Alizain Feerasta specification: 48-bit millisecond timestamp + 80 random bits, Crockford base32 encoded to a 26-character string. ULIDs sort lexicographically by creation time, are fixed-width, and are URL-safe.
|
||||
|
||||
## Features
|
||||
|
||||
- Exactly 26 characters, Crockford base32 (`0-9A-HJKMNP-TV-Z`) — no I/L/O/U to avoid confusion
|
||||
- 48-bit timestamp prefix that sorts correctly through year 10889
|
||||
- Injectable `now?` and `rng?` for deterministic tests
|
||||
- Rejects out-of-range timestamps with typed `RandomError.ARG.OUT_OF_RANGE`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomULID } from "@suaveplan/random/ids";
|
||||
|
||||
const id = randomULID();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomULID } from "@suaveplan/random/ids";
|
||||
|
||||
const rng = new Sfc32Rng("batch-2026-04-19");
|
||||
const ids = Array.from({ length: 1000 }, () => randomULID(Date.now, rng));
|
||||
// Sorted set matches insertion order — handy for offset-paginated APIs.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomULID(now?, rng?): string` — returns a 26-character Crockford base32 ULID.
|
||||
Throws `RandomError.ARG.OUT_OF_RANGE` if `now()` returns a negative number or a value above the 48-bit millisecond ceiling (year 10889).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The timestamp is encoded by repeatedly taking `t % 32` and dividing by 32 for 10 digits, which packs 50 bits of space but only ever uses the lower 48 (the top 2 bits are always zero in the Crockford encoding). Random bits are drawn 5 at a time from `rng.nextUint32()` — a uint32 supplies six 5-bit nibbles, but we take one at a time for simplicity; this allocates 16 calls per ULID. That is cheap on both the seeded and the crypto-backed paths.
|
||||
@@ -0,0 +1,44 @@
|
||||
# ids/uuid
|
||||
|
||||
## Purpose
|
||||
|
||||
The `uuid` module produces RFC 9562-compliant UUIDv4 and UUIDv7 identifiers. UUIDv4 is 122 bits of pure randomness with the version/variant nibbles set; UUIDv7 is a 48-bit timestamp prefix with a 12-bit sequence counter and 62 random bits, producing sortable, collision-resistant IDs that lexicographically order by creation time.
|
||||
|
||||
UUIDv7 is the preferred choice for new primary keys: it is time-sortable like ULID, compatible with every existing UUID tool, and produces no index hot-spots in databases.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomUUID()` — RFC 9562 §5.4 version-4 random UUID
|
||||
- `randomUUIDv7()` — RFC 9562 §5.7 version-7 sortable UUID with same-millisecond monotonicity via a 12-bit sequence counter
|
||||
- Injectable `rng?` for deterministic generation in tests
|
||||
- Injectable `now?` for v7 clock control
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomUUID, randomUUIDv7 } from "@suaveplan/random/ids";
|
||||
|
||||
const correlationId = randomUUID();
|
||||
const orderId = randomUUIDv7();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomUUIDv7 } from "@suaveplan/random/ids";
|
||||
|
||||
const rng = new Mulberry32Rng("test-run");
|
||||
const fixedClock = () => 1_700_000_000_000;
|
||||
const ids = Array.from({ length: 5 }, () => randomUUIDv7(fixedClock, rng));
|
||||
// All 5 IDs share the timestamp prefix and are monotonically ordered by the sequence counter.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomUUID(rng?): string` — 122 random bits, versioned `4`, variant `10xx`.
|
||||
- `randomUUIDv7(now?, rng?): string` — 48-bit timestamp + 12-bit sequence (monotonic across identical timestamps) + 62 random bits, versioned `7`, variant `10xx`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
UUIDv7 maintains two module-level state variables: `lastV7Ts` (the last timestamp observed) and `lastV7Seq` (the rolling 12-bit counter within that timestamp). When the clock advances, `lastV7Seq` is re-seeded from `rng.nextUint32()`; within the same millisecond it simply increments. A 12-bit counter permits 4096 UUIDs per millisecond per process before collision, which comfortably exceeds realistic workloads.
|
||||
@@ -0,0 +1,61 @@
|
||||
# numbers/random-float
|
||||
|
||||
## Purpose
|
||||
|
||||
The `random-float` module produces a uniformly distributed double in a user-specified half-open interval `[min, max)`. The resolution is 32 bits (inherited from the `Rng.nextFloat()` contract), which is more than enough for every non-financial use case in the project.
|
||||
|
||||
## Features
|
||||
|
||||
- Half-open interval `[min, max)`
|
||||
- Accepts any `Rng` or uses the package default
|
||||
- Typed `RandomError` on non-finite or inverted ranges
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers";
|
||||
|
||||
const jitter = randomFloat(-0.1, 0.1);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomFloat } from "@suaveplan/random/numbers";
|
||||
|
||||
const rng = new Mulberry32Rng("layout");
|
||||
const x = randomFloat(0, 1920, rng);
|
||||
const y = randomFloat(0, 1080, rng);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomFloat(min, max, rng?): number` — uniform in `[min, max)`; throws
|
||||
`RandomError.ARG.INVALID` or `ARG.OUT_OF_RANGE` on bad inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The result is `min + nextFloat() * (max - min)`. Because `nextFloat()` returns `[0, 1)`, the output is strictly less than `max` regardless of rounding — you do not need to clamp the upper bound in caller code.
|
||||
|
||||
### Range Semantics
|
||||
|
||||
`randomFloat(min, max)` returns a uniformly distributed value in `[min, max)` — min inclusive, max exclusive — computed by scaling the injected `Rng`'s `nextFloat()` output (which is itself in `[0, 1)`). A `RandomError` is thrown when `min >= max` or either bound is non-finite, so callers never see silently-wrong output.
|
||||
|
||||
## Further Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers/random-float";
|
||||
const temperature = randomFloat(18.5, 24.5); // Celsius, realistic room temp
|
||||
```
|
||||
|
||||
## Further Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomFloat } from "@suaveplan/random/numbers/random-float";
|
||||
import { createSeededRng } from "@suaveplan/pseudo-random";
|
||||
const rng = createSeededRng(42);
|
||||
const draws = Array.from({ length: 1000 }, () => randomFloat(0, 1, { rng }));
|
||||
```
|
||||
|
||||
The exclusive upper bound matches the ECMA-262 `Math.random()` convention, which every Suaveplan consumer already assumes. That exclusive convention is also what makes `randomFloat(0, n)` a safe index-into-array-of-length-n building block for the array helpers in the package.
|
||||
@@ -0,0 +1,45 @@
|
||||
# numbers/random-int
|
||||
|
||||
## Purpose
|
||||
|
||||
The `random-int` module provides an unbiased, inclusive-range integer sampler. Unlike the common `Math.floor(Math.random() * (max - min + 1)) + min` idiom (which is slightly biased whenever the range doesn't divide `2^32`), this implementation uses rejection sampling to guarantee a perfectly uniform distribution even at the extreme edges.
|
||||
|
||||
It accepts an optional `rng?` for reproducibility — pass a `SeededRng` in tests, omit for the crypto-backed default.
|
||||
|
||||
## Features
|
||||
|
||||
- Uniform over `[min, max]` inclusive both ends
|
||||
- Rejection sampling eliminates modulo bias
|
||||
- Typed `RandomError` on out-of-range or non-integer inputs
|
||||
- Works with any `Rng` (or the crypto-backed default)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomInt } from "@suaveplan/random/numbers";
|
||||
|
||||
const d6 = randomInt(1, 6);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomInt } from "@suaveplan/random/numbers";
|
||||
|
||||
const rng = new Sfc32Rng("replay");
|
||||
const hand = Array.from({ length: 5 }, () => randomInt(1, 52, rng));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomInt(min, max, rng?): number` — returns a uniformly random integer in `[min, max]`.
|
||||
Throws `RandomError.ARG.NOT_INTEGER` / `ARG.OUT_OF_RANGE` on bad inputs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The rejection loop divides the uint32 space into equal-sized buckets of size `range` and rejects any draw that falls into the partial trailing bucket. The expected number of draws is `2^32 / limit`, which is never worse than `2` and typically `1.0000…`.
|
||||
|
||||
### Range Semantics
|
||||
|
||||
`randomInt(min, max)` returns a uniformly distributed integer in `[min, max)` using rejection sampling against the injected `Rng`'s `nextUint32` to avoid the modulo-bias that plagues naive `% n` implementations. Both bounds must be safe integers and `min < max` — anything else throws `RandomError`. The rejection loop has an exponentially small probability of iterating more than once.
|
||||
@@ -0,0 +1,52 @@
|
||||
# objects/objects
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `objects` module lifts the collection-random operations onto plain records. It gives you `randomKey`, `randomValue`, `randomEntry`, and `randomSubset` — the four operations you most often want when sampling JSON-shaped configurations, feature flags, or dictionary-backed domain models.
|
||||
|
||||
## Features
|
||||
|
||||
- `randomKey(obj, rng?)` — a single `keyof T` drawn uniformly
|
||||
- `randomValue(obj, rng?)` — the value at a random key
|
||||
- `randomEntry(obj, rng?)` — `[key, value]` tuple
|
||||
- `randomSubset(obj, count, rng?)` — new object with `count` random entries
|
||||
- Every function delegates to the collection-random helpers, so draws use the same unbiased sampler
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomKey, randomSubset } from "@suaveplan/random/objects";
|
||||
|
||||
const config = { alpha: true, beta: false, gamma: true };
|
||||
const someFlag = randomKey(config);
|
||||
const subset = randomSubset(config, 2);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomEntry } from "@suaveplan/random/objects";
|
||||
|
||||
const dictionary = { apple: "red", banana: "yellow", cherry: "red" };
|
||||
const rng = new Mulberry32Rng("quiz");
|
||||
const [word, colour] = randomEntry(dictionary, rng);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `randomKey<T extends object>(obj, rng?): keyof T`
|
||||
- `randomValue<T extends object>(obj, rng?): T[keyof T]`
|
||||
- `randomEntry<T extends object>(obj, rng?): [keyof T, T[keyof T]]`
|
||||
- `randomSubset<T extends object>(obj, count, rng?): Partial<T>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Object.keys` is called once per operation rather than caching, because the object may be mutated between calls. If you need repeated subsets from the same object, extract `Object.keys(obj)` yourself and call `sample` directly from `@suaveplan/random/collections`.
|
||||
|
||||
### Helpers
|
||||
|
||||
`randomElement` returns a uniformly-chosen element from any non-empty array; `shuffle` performs a Fisher-Yates in-place shuffle backed by the injected `Rng`; `sample` returns k elements without replacement using reservoir sampling for stable memory on large inputs. All three throw `RandomError` on invalid input (empty array, `k > arr.length`, `k < 0`).
|
||||
@@ -0,0 +1,48 @@
|
||||
# source/crypto-rng
|
||||
|
||||
## Purpose
|
||||
|
||||
The `crypto-rng` module is the single authoritative binding between `@suaveplan/random` and the platform's cryptographically secure random source. It wraps `globalThis.crypto.getRandomValues` in an `Rng` adapter (so it composes with every helper in this package) and exposes a `randomBytes(n)` factory that handles WebCrypto's 65 536-byte per-call limit by chunking.
|
||||
|
||||
Every other public function in `@suaveplan/random` draws from `cryptoRng()` by default; isolating the CSPRNG here means the `no-disallowed` test can enforce "no other file in src/ touches `crypto.getRandomValues`".
|
||||
|
||||
## Features
|
||||
|
||||
- `cryptoRng()` — returns a fresh `Rng` reading 4 random bytes per `nextUint32`
|
||||
- `randomBytes(length)` — crypto-backed buffer, auto-chunking for large lengths
|
||||
- Typed `RandomError.SOURCE.UNAVAILABLE` when WebCrypto is absent (e.g. some edge sandboxes)
|
||||
- Typed `RandomError.ARG.INVALID` when `length` is not a non-negative integer
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { cryptoRng, randomBytes } from "@suaveplan/random/source";
|
||||
|
||||
const rng = cryptoRng();
|
||||
const draw = rng.nextUint32();
|
||||
const key = randomBytes(32);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { cryptoRng, randomBytes } from "@suaveplan/random/source";
|
||||
import { shuffle } from "@suaveplan/pseudo-random";
|
||||
|
||||
const rng = cryptoRng();
|
||||
const deck = shuffle(["A", "K", "Q", "J"], rng);
|
||||
|
||||
// Session token: 256-bit, URL-safe
|
||||
const token = [...randomBytes(32)]
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `cryptoRng(): Rng` — throws `RandomError.SOURCE.UNAVAILABLE` if `globalThis.crypto.getRandomValues` is missing.
|
||||
- `randomBytes(length: number): Uint8Array` — throws `RandomError.ARG.INVALID` on bad lengths. Chunks at 65 536 bytes to stay within the WebCrypto limit.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`cryptoRng` reuses a single 4-byte buffer between draws to avoid allocation pressure in hot loops. That buffer is captured in the returned object's closure, so each call to `cryptoRng()` gets an independent scratch area and the returned `Rng` is safe to use from a single call stack (the usual JS contract — do not share a single `Rng` instance across unrelated concurrent tasks that expect unique draws, because WebCrypto itself is thread-safe but the shared 4-byte buffer is not).
|
||||
@@ -0,0 +1,49 @@
|
||||
# source/default-rng
|
||||
|
||||
## Purpose
|
||||
|
||||
The `default-rng` module holds the process-wide default `Rng` that every helper in `@suaveplan/random` falls back to when the caller doesn't pass an explicit `rng?`. It is deliberately a single module-local variable (not a class, not a context) so the API stays ergonomic: call `randomString()` with no arguments and get a crypto-backed 16-character token.
|
||||
|
||||
Tests swap the default for a `SeededRng` to make randomness deterministic; production code leaves the default alone.
|
||||
|
||||
## Features
|
||||
|
||||
- `getDefaultRng()` — lazy initialisation, returns a stable `cryptoRng()` on first call
|
||||
- `setDefaultRng(rng)` — dependency-injection hook for tests and custom sources
|
||||
- `resetDefaultRng()` — restore the default, typically called from `afterEach`
|
||||
- Single-module state; no transitive cost when tree-shaken away
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getDefaultRng } from "@suaveplan/random/source";
|
||||
|
||||
const v = getDefaultRng().nextUint32();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
|
||||
import { getDefaultRng, resetDefaultRng, setDefaultRng } from "@suaveplan/random/source";
|
||||
import { afterEach } from "@suaveplan/testing/runner";
|
||||
|
||||
afterEach(() => resetDefaultRng());
|
||||
|
||||
setDefaultRng(new Mulberry32Rng("test"));
|
||||
// ...every @suaveplan/random helper called with no `rng?` now draws from the seeded source
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getDefaultRng(): Rng` — returns the current default, creating a `cryptoRng()` on first access.
|
||||
- `setDefaultRng(rng: Rng): void` — replace the default.
|
||||
- `resetDefaultRng(): void` — clear the cache; next `getDefaultRng()` returns a fresh `cryptoRng()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default is created lazily rather than eagerly so that importing the package in an environment without WebCrypto doesn't throw at import time — it only throws if you actually try to draw. This matters for bundlers that preload tree-shaken modules.
|
||||
|
||||
### Default Source
|
||||
|
||||
`defaultRng` wraps `crypto.getRandomValues` from the ambient Web Crypto API into the canonical `Rng` shape from `@suaveplan/types`. It is the default injection for every helper in the package when the caller does not supply one. On platforms without `crypto.getRandomValues` (pre-Node-19 without `--experimental-global-webcrypto`, for example) the module throws at construction time rather than silently falling back to `Math.random`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# strings/random-string
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `random-string` module produces random strings from one of eight curated charset presets (alpha, alpha-upper, alpha-lower, alphanumeric, numeric, hex, base58, base64url) or from an arbitrary custom alphabet. Every character is drawn with an unbiased `randomInt(0, alphabet.length - 1)`, so the output distribution is perfectly uniform regardless of alphabet length.
|
||||
|
||||
It is the go-to primitive for short random IDs, temporary passwords, invite codes, and any bespoke token scheme where none of the formal ID formats (UUID / ULID / Nanoid) applies.
|
||||
|
||||
## Features
|
||||
|
||||
- Eight curated presets, including Bitcoin-style base58 (no `0/O/I/l`) and URL-safe base64
|
||||
- Custom alphabets for bespoke use cases (presets and custom are mutually exclusive)
|
||||
- Accepts `rng?` for deterministic output in tests
|
||||
- Unbiased character picking via `randomInt` rejection sampling
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { randomString } from "@suaveplan/random/strings";
|
||||
|
||||
const token = randomString({ length: 16, charset: "base58" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Sfc32Rng } from "@suaveplan/pseudo-random";
|
||||
import { randomString } from "@suaveplan/random/strings";
|
||||
|
||||
const code = randomString({
|
||||
length: 6,
|
||||
customCharset: "ABCDEFGHJKMNPQRSTUVWXYZ23456789", // no ambiguous chars
|
||||
rng: new Sfc32Rng("voucher-run-1"),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CHARSETS` — readonly record of the eight built-in charsets.
|
||||
- `CharsetPreset` — `keyof typeof CHARSETS`.
|
||||
- `randomString({ length, charset?, customCharset?, rng? }): string` — throws `RandomError.STRING.MUTUALLY_EXCLUSIVE` if both `charset` and `customCharset` are set, and `STRING.EMPTY_CHARSET` if the resolved alphabet is empty. Defaults to `alphanumeric` when no charset is specified.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
String concatenation by `+=` is slightly slower than `Array.prototype.join` for very long outputs, but for the common case (< 256 characters) the V8 rope-string optimisation makes `+=` the simplest readable choice. Because this module delegates character picking to `randomInt`, alphabet length never introduces modulo bias.
|
||||
Reference in New Issue
Block a user