Probably Playable
Back to Codons

Seeded Random

Stable

Deterministic RNG — same seed, same results, every time

by Probably Playable — v1.0.0

Description

The problem

Math.random() gives you different results every time. Your AI generates a procedural level, but you can't reproduce it. You can't test it, debug it, or share a seed with another player. Your replay system breaks because the random calls don't match.

The solution

A drop-in replacement for Math.random() that's deterministic. Same seed → same sequence, always. 44 lines. Zero dependencies.

  • next() — float in [0, 1)
  • int(min, max) — integer in range
  • pick(array) — random element
  • shuffle(array) — Fisher-Yates in place
  • chance(prob) — true/false with probability
Dependencies

None — zero external dependencies.

Phaser Three.js Vanilla JS Any Framework
Technical Details
Version
1.0.0
Status
Stable
License
MIT
Size
1 KB
Author
Probably Playable
Updated
2025-04-04
randomproceduraldeterministicRNGseedreplay
AI Integration Skill

Drop into .claude/skills/ — your AI handles the rest.

AI Skill codon.skill.md

SeededRandom — Integration Skill

Use this skill when the user asks to "deterministic random", "seeded RNG",

"reproducible random", "procedural generation seed", or "repeatable randomness".

WHAT IT DOES

A deterministic pseudo-random number generator using the mulberry32 algorithm. Given the same integer seed, it always produces the exact same sequence of numbers. This makes it perfect for procedural generation that needs to be reproducible (world gen, loot tables, level layouts), replay systems where inputs must match, and deterministic testing where you need predictable "random" values.

REQUIREMENTS

  • Framework: Any (Phaser, Three.js, vanilla JS, Node.js)
  • Language: TypeScript or JavaScript
  • Dependencies: None
  • Files: 1 TypeScript file (~60 LOC)

INSTALL

  1. Copy the SeededRandom class into your project (e.g., src/utils/SeededRandom.ts).
    1. Import wherever needed:
    2. `typescript

      import { SeededRandom } from './utils/SeededRandom';

      `

      1. Create an instance with an integer seed:
      2. `typescript

        const rng = new SeededRandom(12345);

        `

        INTEGRATION

        Basic Usage

        
        class SeededRandom {
          private state: number;
        
          constructor(seed: number) {
            this.state = seed | 0; // Ensure integer
          }
        
          /** Returns a float in [0, 1) — same seed always gives same sequence */
          next(): number {
            let t = (this.state += 0x6d2b79f5);
            t = Math.imul(t ^ (t >>> 15), t | 1);
            t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
            return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
          }
        
          /** Random integer in [min, max] inclusive */
          int(min: number, max: number): number {
            return Math.floor(this.next() * (max - min + 1)) + min;
          }
        
          /** Pick a random element from an array */
          pick<T>(arr: T[]): T {
            return arr[Math.floor(this.next() * arr.length)];
          }
        
          /** Fisher-Yates shuffle (mutates array in place, returns it) */
          shuffle<T>(arr: T[]): T[] {
            for (let i = arr.length - 1; i > 0; i--) {
              const j = Math.floor(this.next() * (i + 1));
              [arr[i], arr[j]] = [arr[j], arr[i]];
            }
            return arr;
          }
        
          /** Returns true with the given probability (0-1) */
          chance(probability: number): boolean {
            return this.next() < probability;
          }
        }
        

        Procedural Generation Example

        
        // Same seed = same world every time
        const worldRng = new SeededRandom(42);
        
        function generateRoom(): Room {
          return {
            width: worldRng.int(5, 15),
            height: worldRng.int(5, 15),
            enemies: worldRng.int(0, 3),
            hasTreasure: worldRng.chance(0.25),
            theme: worldRng.pick(['cave', 'dungeon', 'forest', 'ruins']),
          };
        }
        

        Replay System Example

        
        // Store seed with replay data
        const replaySeed = Date.now();
        const rng = new SeededRandom(replaySeed);
        
        // During playback, recreate with same seed
        const playbackRng = new SeededRandom(replaySeed);
        // All "random" events will be identical
        

        CONFIGURATION

        OptionTypeDefaultDescription
        seednumber*required*Integer seed for the PRNG. Same seed = same sequence.

        There are no other configuration options. The algorithm is stateless beyond the seed — create multiple instances for independent streams (e.g., one for world gen, one for loot, one for particles).

        API REFERENCE

        MethodSignatureReturnsDescription
        constructornew SeededRandom(seed: number)SeededRandomCreate a new PRNG with the given integer seed
        next.next()numberFloat in [0, 1) — advances internal state
        int.int(min: number, max: number)numberRandom integer in [min, max] inclusive
        pick.pick<T>(arr: T[])TRandom element from array
        shuffle.shuffle<T>(arr: T[])T[]Fisher-Yates in-place shuffle, returns same array
        chance.chance(probability: number)booleantrue with given probability (0 = never, 1 = always)

        GOTCHAS

        • Seed must be an integer. Floats get truncated via | 0. Passing NaN or undefined will produce a degenerate sequence.
        • State mutates with each call. Every call to next(), int(), pick(), shuffle(), or chance() advances the internal state. Calling them in a different order produces a different sequence — keep call order deterministic.
        • Not cryptographically secure. This is for game logic and procedural generation, not security.
        • One instance per independent stream. If your world gen and your particle system both use the same instance, adding a particle call will shift the entire world sequence. Use separate instances with separate seeds.

This codon is provided "as is" without warranty of any kind, express or implied. Probably Playable assumes no responsibility for any damages, data loss, security vulnerabilities, or defects arising from its use. You are solely responsible for reviewing, testing, and validating this code before integrating it into your project. Use at your own risk.