Random
utils/random.ts:115A seedable pseudo-random number generator and a grab-bag of common game-development helpers built on top of it.
Random is a thin, dependency-free wrapper around
mulberry32,
a small, fast 32-bit PRNG with a 2³² period — more than adequate for
gameplay use (particle jitter, loot rolls, spawn placement, deterministic
replays, etc.). It is not cryptographically secure; do not use it where
an attacker controls or observes outputs.
Seeding
Construct with new Random(options). Passing no seed produces a
time-based seed; passing a seed (number or string) produces a fully
deterministic stream — two generators built with the same seed emit the
same sequence forever. String seeds are hashed so named seeds like
'daily-2026-05-20' work as expected.
Determinism and persistence
Use Random.getState / Random.setState (or
new Random({ state })) to snapshot and restore the exact internal state.
This is what powers deterministic replays — capture the state at the start
of a frame, capture the inputs, and re-running both reproduces the frame
exactly.
Sampling helpers
The sampling helpers (Random.inRectangle, Random.inCircle, Random.inRing, Random.inPolygon) return frozen PointPrimitives in absolute coordinates — pass the world-space center or origin of the area you want to sample within. They produce uniformly distributed points (not biased toward the center, which a naive polar sampler would do).
Example
const rng = new Random({ seed: 'level-3' });
rng.between(0, 10); // float in [0, 10)
rng.integer(1, 6); // integer in [1, 6] inclusive (a d6 roll)
rng.boolean(0.25); // 25% chance of true
rng.pick(['rock', 'paper']); // one of the items, uniformly
rng.inCircle(100, 100, 50); // uniform point in a 50-radius disc
rng.color(); // random 24-bit colorconst a = new Random({ seed: 42 });
a.next(); a.next();
const b = new Random({ state: a.getState() });
b.next() === a.next(); // true — same state, same next valueConstructors
constructor(options: RandomOptions): Random Creates a new generator.
Parameters
-
optionsRandomOptions
Properties
seed: number The 32-bit unsigned integer seed this generator was constructed with. Stable for the lifetime of the instance — even after the internal state has advanced — so it can be logged or echoed back in a save file to reproduce a run.
When the generator was constructed with state rather than seed, the
seed is the value of that initial state.
Methods
angle(): number Returns a uniformly distributed angle in radians, in the range [0, 2π).
Pair with Math.cos / Math.sin for a random unit vector.
Returns
number between(min: number, max: number): number Returns a uniformly distributed float in the range [min, max). If
min is greater than max the arguments are swapped, so the caller
does not need to pre-sort them. When min === max the result is exactly
min.
Parameters
-
minnumber -
maxnumber
Returns
number boolean(probability: number): boolean Returns true with the given probability, false otherwise. The default
probability of 0.5 makes the call read as a fair coin flip.
Probabilities <= 0 always return false; probabilities >= 1 always
return true. The PRNG is still advanced in those cases, keeping the
stream deterministic regardless of the threshold.
Parameters
-
probabilitynumber
Returns
boolean color(min: number, max: number): number Returns a 24-bit color as a number in the range [min, max], suitable
for passing to PIXI tinting / fill APIs. The default range covers the
full RGB space (0x000000 to 0xffffff).
Bounds outside [0x000000, 0xffffff] are clamped, and a min greater
than max is swapped. Note this samples integer colors uniformly over
the numeric range — it does not perform any perceptually uniform sampling
in HSL/LAB space.
Parameters
-
minnumber -
maxnumber
Returns
number getState(): number Captures the current internal state of this generator as a 32-bit
unsigned integer. Pass the result back via new Random({ state }) (or
Random.setState) to recreate a generator that resumes the stream
from exactly this point.
Returns
number hexString(length: number): string Returns a string of random lowercase hexadecimal characters of the given
length. Useful for ad-hoc ids, mock asset hashes, debug labels, and so
on. Each character is drawn independently from 0-9a-f.
Non-positive or non-integer lengths produce an empty string.
Parameters
-
lengthnumber
Returns
string inCircle(x: number, y: number, radius: number): Readonly Returns a frozen point uniformly distributed within the closed disc
centred at (x, y) with the given radius. Uses the
r = R * sqrt(u) mapping to avoid the bias toward the center that a
naive (r, θ) sampler produces.
A radius of 0 (or negative, which is treated as its absolute value)
returns the center point.
Parameters
-
xnumber -
ynumber -
radiusnumber
Returns
Readonly inFrame(x: number, y: number, width: number, height: number, thickness: number): Readonly Returns a frozen point uniformly distributed within the rectangular
frame described by an outer rectangle of width × height anchored at
(x, y) (its top-left, screen-space) and a uniform inset thickness.
This is the rectangle analog of Random.inRing: sampling lands
anywhere in the band between the outer rectangle and the inner rectangle
carved out by the inset, never inside the inner hole.
Sampling is exact (not rejection-based): the four border strips are weighted by area and one is picked, then the point is drawn uniformly within it. Cost is constant regardless of how thin the frame is.
Edge cases:
- When
thickness * 2 >= min(width, height)the frame fills the entire outer rectangle (no interior hole left), and the result is equivalent to Random.inRectangle. - When
thickness <= 0the frame has zero area; the call returns the outer rectangle's top-left corner(x, y)without consuming a draw from the underlying PRNG. - Negative
widthorheightare treated as their absolute value, with(x, y)remaining the top-left corner of the resulting outer box.
Parameters
-
xnumber -
ynumber -
widthnumber -
heightnumber -
thicknessnumber
Returns
Readonly inPolygon(vertices: readonly PointPrimitive[]): Readonly Returns a frozen point uniformly distributed inside the given simple (non-self-intersecting) polygon, expressed by its vertices in absolute coordinates. Uses bounding-box rejection sampling backed by Polygon.containsPoint: simple and correct for any shape, but cost grows with how much of the bounding box the polygon doesn't fill.
For pathological inputs (a polygon with effectively zero area, or extreme aspect ratio that defeats rejection sampling), the method gives up after a bounded number of attempts and falls back to the polygon's area centroid so callers still get a sensible point inside the shape.
Polygons with fewer than three vertices are degenerate; the first
vertex (or (0, 0) for an empty array) is returned instead.
Parameters
-
verticesreadonly PointPrimitive[]
Returns
Readonly inRectangle(x: number, y: number, width: number, height: number): Readonly Returns a frozen point uniformly distributed within the axis-aligned
rectangle described by the given origin and size. The origin is the
rectangle's top-left corner in screen-space (y increases downward).
Negative width or height are treated as their absolute value, with
the origin still the top-left corner of the resulting box.
Parameters
-
xnumber -
ynumber -
widthnumber -
heightnumber
Returns
Readonly inRing(x: number, y: number, minRadius: number, maxRadius: number): Readonly Returns a frozen point uniformly distributed within the annulus (ring)
centred at (x, y), bounded by minRadius and maxRadius. Uses the
r = sqrt(u * (R² - r²) + r²) mapping so points are uniform over the
ring's area, not its radius.
If minRadius is greater than maxRadius the arguments are swapped.
Negative radii are treated as their absolute value.
Parameters
-
xnumber -
ynumber -
minRadiusnumber -
maxRadiusnumber
Returns
Readonly integer(min: number, max: number): number Returns a uniformly distributed integer in the range [min, max], both
ends inclusive — the conventional "roll a d6" semantics. Non-integer
bounds are floored toward zero; if min is greater than max the
arguments are swapped.
Parameters
-
minnumber -
maxnumber
Returns
number next(): number Advances the generator and returns a uniformly distributed float in the
range [0, 1). This is the raw output of the underlying PRNG; every
other helper on Random is built on top of it.
Returns
number pick(items: readonly T[]): T Picks one element from items uniformly at random.
Parameters
-
itemsreadonly T[]
Returns
T The chosen element.
Throws
An EngineError with code
ErrorCode.RANDOM_EMPTY_ITEMS when items is empty — picking from
nothing is always a programming error.
setState(state: number): this Restores this generator's internal state to a value previously captured by Random.getState. Non-finite inputs are ignored.
Parameters
-
statenumber
Returns
this This generator, for chaining.
shuffle(items: T[]): T[] Shuffles items in place using a Fisher–Yates pass and returns the same
array, so the call can be chained or its result captured inline. An
empty or single-element array is returned untouched.
Parameters
-
itemsT[]
Returns
T[] The same array reference, now shuffled.
sign(): -1 | 1 Returns either -1 or 1 with equal probability. Useful for randomising
a velocity direction or flipping a sprite.
Returns
-1 | 1