Add Random.Product for deterministic replay and serialization

Summary

I would like to request exposing the internal state of Random through a read-only Product property (or equivalent), along with support for constructing a Random object from that value.

Example:

local rng = Random.new(12345)

local product = rng.Product

local restored = Random.new(product)

print(rng:NextInteger(1, 100))
print(restored:NextInteger(1, 100))
-- Always produces identical results.

Alternatively, if Product already represents the internal state after seed initialization, the constructor could simply accept either a seed or a previously generated product transparently.


Motivation

Currently, Random.new(seed) guarantees deterministic output only when starting from the beginning of the sequence.

However, once several random values have already been generated, there is no way to serialize the current state of the generator and resume from that exact point later.

Exposing the generator’s current state would make Random fully deterministic and resumable.

This would enable:

  • Save/load systems for procedural generation.
  • Network synchronization.
  • Deterministic replay systems.
  • Checkpointing simulations.
  • Debugging by reproducing an exact random sequence.
  • Serialization of generator state.

Example

Without this feature:

local rng = Random.new(42)

rng:NextNumber()
rng:NextNumber()

-- There is currently no way to recreate this exact state.

With this feature:

local rng = Random.new(42)

rng:NextNumber()
rng:NextNumber()

local product = rng.Product

-- Save product...

local restored = Random.new(product)

-- Continues from the exact same point.

Why not just store the seed?

A seed only represents the initial state.

After any random values are generated, the internal state changes.

Reconstructing the generator by using only the original seed requires replaying every previous random call, which is inefficient and often impossible if the exact history is unavailable.

Saving the current generator state avoids this entirely.


Proposed API

local rng = Random.new(seed)

local state = rng.Product -- read-only

local restored = Random.new(state)

or, if preferred,

local rng = Random.new(seed)

local state = rng.Product

local restored = Random.fromProduct(state)

Benefits

  • Makes Random fully serializable.
  • Enables deterministic replay.
  • Simplifies debugging.
  • Improves procedural generation workflows.
  • Eliminates the need to replay an entire sequence just to restore generator state.
  • Minimal API surface: only a single read-only property (and optionally a dedicated constructor) is required.

I believe this would significantly improve the usability of Random while remaining backwards compatible.

2 Likes

so basically, you want to make it so for example, I have this

local randomRoll = Random.new(23456)

local ResumedKey = randomRoll.Product -- this is the number given
local newRoll = Random.new(ResumedKey)

print(randomRoll:NextInteger(1, 100)) -- this is a new roll, completely random
print(newRoll:NextInteger(1, 100)) -- basically, using the old roll number and integerizing it

It would use the old Product(randomRoll’s number) and you could reroll using that number?

1 Like

Yes, exactly.
This 2 results would be identical
because currently the only way to archive this would be to create history and then reroll it every time you want to create a new generator :skull:
Which is both inefficient and annoying and reinvents the wheel for something that should’ve already been a thing.
Exposing the product shouldn’t be too hard.
Because it’s very necessary if you want branching randomness or recreatability in isolated areas.

kind of like a seed, for example, 12345

since Roblox doesn’t let to save to machine, you have to either save it into a datastore or forget it, and your saying that .Product would refetch that seed(12345)?

The seed and product are the same thing.
The seed is referring to the initial value when the product is altered seed (look up how pseudo-random generators work).

so basically,

if I were to do

local randomRoll = Random.new(23456)

local ResumedKey = randomRoll.Product -- this is the number given
local newRoll = Random.new(ResumedKey)

print(randomRoll:NextInteger(1, 100)) -- this is a new roll, completely random
print(newRoll:NextInteger(1, 100)) -- basically, using the old roll number and integerizing it

the product and randomRoll would be exactly the same?

Well, you did not alter the seed, and I’m not sure if Roblox does preprocessing of the seed besides clamping, so in that case it would be the same.
Product should change only when you call NextNumber or NextInteger
better example would be:

local rng = Random.new()
rng:NextNumber()
rng:NextNumber()
rng:NextNumber()
local seed = rng.Product
print(rng:NextNumber())
print(Random.new(seed):NextNumber())

All of them return the same value.

well, they all return the same value, because the PRNG you told me to search up is based off a deterministic math function and basically it would create a long sequence that looks random, but is actually fully determined by the original seed.

Kinda like minecraft in a way, but minecraft is perlin noise, either way, if you use a seed, it will generate the same terrain and map everytime(kinda false because generation does change during versions, but still)

Let’s keep the discussion focused on the implementation rather than personal knowledge, as that’s the topic being discussed.

I was just trying to get a better understanding, but ok