156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
/**
|
|
* 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
|