125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
/**
|
|
* Deterministic pseudo-random numbers.
|
|
*
|
|
* Every random decision in the game — drops, affix rolls, monster spawn jitter —
|
|
* goes through one of these, seeded from a value the caller controls. That is not
|
|
* about quality (this is not a cryptographic generator) but about *reproducibility*:
|
|
* a drop table that cannot be replayed cannot be tested, and a simulation whose
|
|
* randomness comes from `Math.random` can never be reconciled between two machines,
|
|
* which is exactly what lockstep multiplayer needs at M5.
|
|
*
|
|
* The algorithm is mulberry32: 32 bits of state, a handful of integer ops, and a
|
|
* period long enough for a game session. It is written here rather than pulled in
|
|
* because the whole engine has no runtime dependencies.
|
|
*/
|
|
|
|
/** A seeded random source. */
|
|
export class Rng {
|
|
private state: number
|
|
|
|
/**
|
|
* @param seed - any 32-bit integer; the same seed replays the same sequence.
|
|
*/
|
|
constructor(seed: number) {
|
|
this.state = seed >>> 0
|
|
}
|
|
|
|
/**
|
|
* The generator's current state.
|
|
*
|
|
* Exposed so a save can capture the stream exactly: restoring a game without its
|
|
* random state would replay the same drops in a different order, which is the
|
|
* kind of difference that only shows up much later.
|
|
*/
|
|
get seed(): number {
|
|
return this.state
|
|
}
|
|
|
|
/**
|
|
* Draw the next value.
|
|
*
|
|
* @returns a number in `[0, 1)`.
|
|
*/
|
|
next(): number {
|
|
this.state = (this.state + 0x6d2b79f5) >>> 0
|
|
let t = this.state
|
|
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
}
|
|
|
|
/**
|
|
* Draw an integer in an inclusive range.
|
|
*
|
|
* @param min - lower bound.
|
|
* @param max - upper bound (inclusive).
|
|
* @returns the integer.
|
|
*/
|
|
int(min: number, max: number): number {
|
|
if (max <= min) return Math.floor(min)
|
|
return Math.floor(min + this.next() * (max - min + 1))
|
|
}
|
|
|
|
/**
|
|
* Draw a floating-point value in a range.
|
|
*
|
|
* @param min - lower bound.
|
|
* @param max - upper bound.
|
|
* @returns the value.
|
|
*/
|
|
range(min: number, max: number): number {
|
|
return min + this.next() * (max - min)
|
|
}
|
|
|
|
/**
|
|
* Draw one element.
|
|
*
|
|
* @param items - the candidates.
|
|
* @returns the element, or undefined when there are none.
|
|
*/
|
|
pick<T>(items: readonly T[]): T | undefined {
|
|
if (items.length === 0) return undefined
|
|
return items[Math.floor(this.next() * items.length)]
|
|
}
|
|
|
|
/**
|
|
* Decide a yes/no with a probability.
|
|
*
|
|
* @param chance - probability in `[0, 1]`.
|
|
* @returns true when the draw succeeds.
|
|
*/
|
|
chance(probability: number): boolean {
|
|
return this.next() < probability
|
|
}
|
|
|
|
/**
|
|
* Derive an independent stream, so one subsystem's draws cannot shift another's.
|
|
*
|
|
* @param label - a stable name for the stream.
|
|
* @returns a new generator.
|
|
*/
|
|
fork(label: string): Rng {
|
|
let hash = 2166136261
|
|
for (let index = 0; index < label.length; index += 1) {
|
|
hash ^= label.charCodeAt(index)
|
|
hash = Math.imul(hash, 16777619) >>> 0
|
|
}
|
|
return new Rng((hash ^ this.state) >>> 0)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A stable 32-bit hash of a string, for seeding streams from names.
|
|
*
|
|
* @param text - the input.
|
|
* @returns the hash.
|
|
*/
|
|
export function hashString32(text: string): number {
|
|
let hash = 2166136261
|
|
for (let index = 0; index < text.length; index += 1) {
|
|
hash ^= text.charCodeAt(index)
|
|
hash = Math.imul(hash, 16777619) >>> 0
|
|
}
|
|
return hash >>> 0
|
|
}
|