npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-random

v1.0.5

Published

Zero-dependency seeded RNG for games — Mulberry32 PRNG with weighted drops, gaussian distribution, shuffle, and 2D vector helpers.

Readme

@zakkster/lite-random

npm version npm bundle size npm downloads npm total downloads TypeScript Zero Dependencies License: MIT

Zero-dependency seeded RNG built for games, not data science.

Deterministic. Fast. Zero allocations. Game-ready API out of the box.

Why This Library?

Most RNG libraries on npm are built for statisticians. They return closures and thunks (random.uniform(0, 1)()), creating garbage in a tight 60fps game loop. They ship Poisson distributions when you need a dice roll.

@zakkster/lite-random is different:

  • No GC spikes — direct execution (rng.range(0, 1)), zero allocations per call
  • Deterministic replay — same seed = same sequence, every time. Replay bugs, test edge cases, ship replays
  • Game-ready API — loot drops (weighted()), critical hits (chance()), 2D physics (unitVector(), sign())
  • Mulberry32 PRNG — entire state in a single 32-bit integer. Blazing fast, minimal memory
  • Natural distributiongaussian() for particle spread, procedural terrain, organic variation
  • Zero dependencies — < 1KB minified

You don't need a statistics library to make a scratch card. You need rng.chance(0.05) and rng.weighted().

Installation

npm install @zakkster/lite-random

Quick Start

import { Random } from '@zakkster/lite-random';

const rng = new Random(42); // deterministic seed

rng.next();           // 0.0–1.0 float
rng.range(5, 10);     // float in [5, 10)
rng.int(1, 6);        // integer 1–6 inclusive (dice roll)
rng.chance(0.2);      // 20% chance → true/false
rng.bool();           // 50/50
rng.sign();           // -1 or 1
rng.pick(['a','b']);   // random element
rng.gaussian(0, 1);   // normal distribution

Benchmarks & Comparison

Micro‑Benchmarks (Chrome M1, 2026)

| Operation | Ops/sec | |------------------|---------| | rng.next() | ~200M | | rng.range() | ~180M | | rng.int() | ~170M | | rng.chance() | ~190M | | rng.gaussian() | ~40M |

Comparison

| Feature | lite‑random | Math.random | random-js | seedrandom | |---------|-------------|-------------|-----------|------------| | Deterministic | ✔ | ✘ | ✔ | ✔ | | Zero allocations | ✔ | ✔ | ✘ | ✔ | | Game‑focused API | ✔ | ✘ | ✘ | ✘ | | <1KB | ✔ | ✔ | ✘ | ✘ | | Weighted selection | ✔ | ✘ | ✔ | ✘ | | Gaussian | ✔ | ✘ | ✔ | ✘ | | unitVector() | ✔ | ✘ | ✘ | ✘ |

API Reference

| Method | Returns | Description | |--------|---------|-------------| | new Random(seed?) | | Create RNG. Defaults to Date.now(). | | .next() | number | Float in [0, 1) | | .reset(seed?) | | Reset to seed (or original). Replay levels. | | .range(min, max) | number | Float in [min, max) | | .int(min, max) | number | Integer in [min, max] inclusive | | .chance(p) | boolean | True with probability p | | .bool() | boolean | 50/50 | | .sign() | -1 \| 1 | Random direction multiplier | | .unitVector() | {x, y} | Normalized 2D direction | | .gaussian(mean?, std?) | number | Normal distribution (Box-Muller) | | .pick(arr) | T \| null | Random element | | .shuffle(arr) | T[] | New shuffled array | | .shuffleInPlace(arr) | T[] | Mutates in-place (GC-friendly) | | .weighted(items, weights) | T | Weighted random selection | | .pickWeighted(...) | T | Alias for .weighted() |

Recipes

Weighted Loot Drops

The core of any game economy. Weights don't need to sum to 100 — they're relative:

const loot = rng.weighted(
    ['Common', 'Rare', 'Epic', 'Legendary'],
    [60, 25, 10, 5]
);

// With item objects
const drop = rng.weighted(
    [{ name: 'Potion', value: 10 }, { name: 'Sword', value: 500 }],
    [90, 10]
);

Procedural Dungeon Generation

Build deterministic levels that play the same every time for the same seed:

const rng = new Random(levelSeed);

for (const room of rooms) {
    if (rng.chance(0.3)) spawnEnemies(room);
    if (rng.chance(0.1)) spawnTreasure(room);
    if (rng.chance(0.02)) spawnBoss(room);
}

Deterministic Particle Burst

Reset the seed before a burst so the pattern is always identical — perfect for polished VFX:

rng.reset(123);
for (let i = 0; i < 20; i++) {
    const dir = rng.unitVector();
    emitter.emit({
        vx: dir.x * rng.range(100, 300),
        vy: dir.y * rng.range(100, 300),
        life: rng.range(0.5, 1.5),
    });
}

Weighted Enemy Spawning

Harder enemies become more common as the player progresses:

const wave = rng.weighted(
    ['Slime', 'Goblin', 'Orc', 'Dragon'],
    [Math.max(0, 60 - level * 5), 25, 10 + level, 5 + level * 2]
);

Random Walk / Brownian Motion

Simple but effective for fireflies, dust particles, or ambient movement:

function updateFirefly(firefly, rng) {
    firefly.x += rng.range(-1, 1);
    firefly.y += rng.range(-1, 1);
}

Natural-Looking Particle Spread

gaussian() clusters values near the center with natural falloff — much better than flat range() for particle effects:

emitter.emitBurst(50, () => ({
    x: origin.x + rng.gaussian(0, 30),  // clustered near center
    y: origin.y + rng.gaussian(0, 30),
    vx: rng.gaussian(0, 50),             // most go slow, few go fast
    vy: rng.gaussian(-100, 40),           // mostly upward
    life: 1 + rng.gaussian(0, 0.3),      // ~1s ± 0.3s
}));

Shuffle a Deck of Cards

In-place shuffle for zero allocations:

const deck = rng.shuffleInPlace(cards);   // mutates, GC-friendly
const hand = rng.shuffle(deck);           // copy, original intact

Deterministic Replay

The killer feature for bug reports and competitive games:

// Record
const gameSeed = Date.now();
const rng = new Random(gameSeed);
saveReplay({ seed: gameSeed, inputs: [...] });

// Replay — identical sequence guaranteed
const rng = new Random(replay.seed);

Random Sign for Directional Variety

// Random left/right velocity
particle.vx = rng.sign() * rng.range(50, 150);

// Random clockwise/counterclockwise spin
particle.rotation = rng.sign() * rng.range(1, 5);

Deterministic Replay (VCR Engine)

The killer feature for bug reports, competitive games, and rollback netcode. By feeding the same seed and the same inputs into your game loop, your game will play out pixel-perfect every single time.

Below is a complete, framework-agnostic ReplayManager using delta-recording (keyframing) to keep memory usage tiny.

import {Random} from '@zakkster/lite-random';

export class ReplayManager {
    /**
     * @param {Object} defaultInput - The baseline input schema for the game
     */
    constructor(defaultInput = {}) {
        this.defaultInput = defaultInput;
        this.mode = 'live';
        this.seed = null;
        this.rng = null;

        this.frames = [];      // [{ frame, input }]
        this.frameIndex = 0;   // Array pointer for reading replays
        this.frame = 0;        // Master timeline counter
        this.playbackSpeed = 1;

        this._lastLiveInput = null;
        this._currentReplayInput = null;
    }

    startLive(seed = Date.now()) {
        this.mode = 'live';
        this.seed = seed;
        this.rng = new Random(seed);
        this.frames = [];
        this.frame = 0;
        this.frameIndex = 0;
        this.playbackSpeed = 1;
        this._lastLiveInput = null;
    }

    startReplay(replayData) {
        this.mode = 'replay';
        this.seed = replayData.seed;
        this.rng = new Random(this.seed);
        this.frames = replayData.frames || [];
        this.frame = 0;
        this.frameIndex = 0;
        this.playbackSpeed = 1;
        this._currentReplayInput = structuredClone(this.defaultInput);
    }

    recordInput(inputState) {
        if (this.mode !== 'live') return;

        if (!this._lastLiveInput || this._hasInputChanged(inputState)) {
            // Native, deep, high-performance cloning
            const clonedInput = structuredClone(inputState);

            this.frames.push({frame: this.frame, input: clonedInput});
            this._lastLiveInput = clonedInput;
        }
    }

    getInput(liveInputState) {
        if (this.mode === 'live') return liveInputState;

        const nextKeyframe = this.frames[this.frameIndex];

        if (nextKeyframe && this.frame >= nextKeyframe.frame) {
            this._currentReplayInput = nextKeyframe.input;
            this.frameIndex++;
        }

        return this._currentReplayInput;
    }

    nextFrame() {
        this.frame += this.playbackSpeed;
    }

    isReplayFinished() {
        return this.mode === 'replay' && this.frameIndex >= this.frames.length;
    }

    jumpToFrame(targetFrame) {
        if (this.mode !== 'replay') return;

        this.frame = targetFrame;

        // Find the last keyframe that occurred BEFORE or ON our target frame
        // (Reverse loop is usually faster since we often scrub near the end)
        let foundIndex = 0;
        for (let i = this.frames.length - 1; i >= 0; i--) {
            if (this.frames[i].frame <= targetFrame) {
                foundIndex = i;
                break;
            }
        }

        this.frameIndex = foundIndex + 1;
        this._currentReplayInput = this.frames[foundIndex]?.input || structuredClone(this.defaultInput);
    }

    getRng() {
        return this.rng;
    }

    toJSON() {
        return {seed: this.seed, frames: this.frames};
    }

    _hasInputChanged(newState) {
        // Deep comparison for nested objects (can be optimized if schema is flat)
        return JSON.stringify(this._lastLiveInput) !== JSON.stringify(newState);
    }
}

</details>

TypeScript

Full generic support — pick(), shuffle(), and weighted() preserve element types:

import { Random } from '@zakkster/lite-random';

const rng = new Random(42);
const item: string = rng.pick(['sword', 'shield', 'potion'])!;
const shuffled: number[] = rng.shuffle([1, 2, 3, 4, 5]);

License

MIT