/** * 1.13c Diablo II Linear Congruential Generator (LCG) PRNG and Per-Unit Seed. * * Gold standard references: * - D2Common!6FD510B0 (SEED_RollRandomNumber / SEED_RollLimitedRandomNumber) * - D2Common!6FD51180 (SEED_InitLowSeed / SEED_InitSeed) * - D2Game!6FC211D0 (Unit seed operations / UNITS_RollRandom) * * In Diablo II 1.13c, PRNG state is maintained as a 64-bit value split into * two 32-bit registers (nLowSeed and nHighSeed). The recurrence relation is: * state64 = (uint64_t)nLowSeed * 0x6AC690C5 + nHighSeed * nLowSeed = (uint32_t)state64 * nHighSeed = (uint32_t)(state64 >> 32) * * Each unit (monsters, players, items, missiles) encapsulates its own seed state, * preventing cross-entity RNG stream contamination and ensuring lockstep determinism. */ /** Canonical 64-bit LCG multiplier in Diablo II 1.13c (0x6AC690C5 = 1791398085). */ export const D2_PRNG_MULTIPLIER = 0x6ac690c5n /** Seed state representing the low and high 32-bit registers. */ export interface D2SeedState { lo: number hi: number } /** * 1.13c Diablo II PRNG implementation with per-unit seed encapsulation. */ export class D2Rng { private _lo = 0 private _hi = 0 /** * Initializes a new PRNG instance. * * @param seed - Optional initial 32-bit low seed or { lo, hi } state object. * @param hi - Optional initial 32-bit high seed (defaults to 0 if not provided). */ constructor(seed?: number | D2SeedState, hi?: number) { if (seed !== undefined) { if (typeof seed === 'number') { this.setSeed(seed, hi) } else { this.setSeed(seed.lo, seed.hi ?? hi) } } } /** Current 32-bit low seed register. */ get lo(): number { return this._lo } /** Current 32-bit high seed register. */ get hi(): number { return this._hi } /** * Updates the seed state registers. * * @param lo - Low 32-bit seed value. * @param hi - High 32-bit seed value (defaults to 0). */ setSeed(lo: number, hi?: number): void { this._lo = lo >>> 0 this._hi = (hi ?? 0) >>> 0 } /** * Returns the current seed state as a { lo, hi } pair. */ getSeed(): D2SeedState { return { lo: this._lo, hi: this._hi } } /** * Advances the 64-bit LCG state by one step: * state64 = BigInt(lo) * 0x6AC690C5n + BigInt(hi) * lo = Number(state64 & 0xFFFFFFFFn) >>> 0 * hi = Number((state64 >> 32n) & 0xFFFFFFFFn) >>> 0 */ step(): void { const state64 = BigInt(this._lo) * D2_PRNG_MULTIPLIER + BigInt(this._hi) this._lo = Number(state64 & 0xffffffffn) >>> 0 this._hi = Number((state64 >> 32n) & 0xffffffffn) >>> 0 } /** * Rolls a pseudo-random integer in `[0, max - 1]`. * * If max <= 0, returns 0 without advancing state. * Otherwise advances state (`step()`). * If max is a power of 2 (i.e. `(max & (max - 1)) === 0`): * returns `(lo & (max - 1)) >>> 0` * Else: * returns `(lo % max) >>> 0` * * @param max - Exclusive upper bound. * @returns Generated random integer in `[0, max - 1]`, or 0 if max <= 0. */ rand(max: number): number { if (max <= 0) { return 0 } this.step() if ((max & (max - 1)) === 0) { return (this._lo & (max - 1)) >>> 0 } return (this._lo % max) >>> 0 } /** * Rolls a pseudo-random integer in inclusive range `[min, max]`. * * If min >= max, returns min without advancing state. * Otherwise span = max - min + 1, advances state (`step()`), * and returns `min + ((lo % span) >>> 0)`. * * @param min - Lower bound (inclusive). * @param max - Upper bound (inclusive). * @returns Random integer in [min, max]. */ randRange(min: number, max: number): number { if (min >= max) { return min } const span = max - min + 1 this.step() return min + ((this._lo % span) >>> 0) } /** * Creates an independent copy of this PRNG instance preserving the exact current seed state. */ clone(): D2Rng { const copy = new D2Rng() copy.setSeed(this._lo, this._hi) return copy } /** * Advances state and returns the updated 32-bit low seed value (matching SEED_RollRandomNumber). */ next(): number { this.step() return this._lo } } /** Alias for D2Rng to represent per-unit seed instances. */ export const UnitSeed = D2Rng export type UnitSeed = D2Rng