Files
vanity-delve/foundry-module/module/core/rng.mjs
T
tevansandClaude Opus 5 9d953dba94 Make the module installable: vendor core, point at release assets
What was on git could not be installed. foundry-module/ referenced core/ rather
than containing it, so an installed copy had no generator at all — it only ever
worked here because the live module directory had core/ copied in by hand.

core/ is now vendored into foundry-module/module/core/, minus cli.mjs and
test.mjs which are desk tools. module.json points manifest and download at
release assets, the way mapwright distributes, so a remote server can install by
URL without reaching into the repo tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:18:23 +01:00

111 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Seeded randomness with derived streams.
*
* Draft 5 §11: a delve is reproducible from its skeleton seed. That only holds if every
* component draws from its *own* stream — otherwise materialising areas in a different order
* changes every subsequent draw. So each component derives a named sub-seed:
*
* seed::skeleton
* seed::area::a3::feature
* seed::area::a3::encounter
*
* Two rules make this work:
* 1. Never share an Rng between components.
* 2. Never let draw *count* in one component affect another.
*/
/** xmur3: string → 32-bit seed. */
function xmur3(str) {
let h = 1779033703 ^ str.length;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return () => {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
/** sfc32: small, fast, good enough for content generation. */
function sfc32(a, b, c, d) {
return () => {
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
let t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
export class Rng {
constructor(seedString) {
this.seed = String(seedString);
const h = xmur3(this.seed);
this._next = sfc32(h(), h(), h(), h());
this.draws = 0;
}
/** A new independent stream. Order-independent by construction. */
derive(...parts) {
return new Rng(`${this.seed}::${parts.join('::')}`);
}
float() { this.draws++; return this._next(); }
/** Integer in [0, n). */
int(n) { return Math.floor(this.float() * n); }
/** Integer in [min, max] inclusive. */
between(min, max) { return min + this.int(max - min + 1); }
chance(p) { return this.float() < p; }
pick(arr) {
if (!Array.isArray(arr) || arr.length === 0) return undefined;
return arr[this.int(arr.length)];
}
/** n distinct items, or as many as exist. Never returns undefined holes. */
pickN(arr, n) {
const pool = [...(arr ?? [])];
const out = [];
while (n-- > 0 && pool.length) out.push(pool.splice(this.int(pool.length), 1)[0]);
return out;
}
shuffle(arr) {
const a = [...(arr ?? [])];
for (let i = a.length - 1; i > 0; i--) {
const j = this.int(i + 1);
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
/** Weighted pick from {key: weight} or [[value, weight], ...]. */
weighted(spec) {
const entries = Array.isArray(spec) ? spec : Object.entries(spec);
const total = entries.reduce((s, [, w]) => s + w, 0);
if (total <= 0) return undefined;
let r = this.float() * total;
for (const [value, w] of entries) { r -= w; if (r < 0) return value; }
return entries[entries.length - 1][0];
}
/** d6 pool; VANITY counts 5–6 as Successes. Used for generation only, never table dice. */
pool(n) { return Array.from({ length: n }, () => this.between(1, 6)); }
}
/** A random, human-sayable seed — two words and a number. */
export function coinSeed(rng = new Rng(String(Date.now()))) {
const a = ['gilded', 'hollow', 'salt', 'ember', 'quiet', 'crooked', 'pale', 'brazen'];
const b = ['court', 'crown', 'mirror', 'vigil', 'debt', 'mask', 'hymn', 'tally'];
return `${rng.pick(a)}-${rng.pick(b)}-${rng.between(100, 999)}`;
}