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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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" });
```

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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.

View File

@@ -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.