Skip to content

Random

local Random = require("@game/ServerScriptService/Twill/Random")
local prize = Random.Pick(lootTable)
local code = Random.Id(16)

Server only, because an outcome a client can draw is an outcome it can choose.

Every draw comes from a cryptographic generator rather than from math.random, so knowing every earlier result says nothing about the next one.

A round makes an outcome auditable. Publish its commitment before anything is drawn, reveal the seed afterwards, and the result is provably older than the announcement.

local round = Random.Commit()
announce(round.Commitment)
local prize = round:Pick(lootTable)
announce(round:Reveal())

Anyone checks it with Random.Verify(commitment, seed) and then replays the same draws with Random.Replay(seed).

A round holds 32 seed bytes. Its commitment is the SHA256 of those bytes, and its reveal is the same bytes as hexadecimal. Draws come from keyed Blake3 over the seed, 64 bytes of stream at a time, so any implementation of Blake3 reproduces them.

The stream is ordered. Replaying a different sequence of calls produces a different sequence of results, and the audit then fails for a reason that has nothing to do with honesty.

For the full flow, see Run a draw a player can audit.

Pick and Shuffle treat every entry as equally likely. For a table where some entries come up more often than others, use Chance, which takes a round from here and draws from its stream, so a weighted outcome is as auditable as an even one.

[Server]

Returns the requested number of bytes that nobody can predict.

function Random.Bytes(count: number): buffer

Returns

buffer - That many unpredictable bytes.

Throws when the count is not a whole number of zero or more.

[Server]

Returns a whole number between the bounds, both of them reachable.

function Random.Int(min: number, max: number): number

Returns

number - A value between the bounds, every one equally likely.

Throws on fractional bounds, an inverted pair, or a span wider than 32 bits. min == max is legal and returns that value.

Values that do not divide evenly into the generator’s word are drawn again rather than folded, because folding makes the low outcomes likelier than the high ones. That is the bias a loot table must not have, and it is why the span is bounded.

[Server]

Returns a fraction between the bounds.

function Random.Number(min: number?, max: number?): number

Parameters

NameTypeDescription
minnumber?Lowest value that may come back. Zero when left out.
maxnumber?Highest value that may come back. One when left out.

Returns

number - A value between the bounds.

Throws when either bound is given and is not finite.

[Server]

Returns one entry of a list, every entry equally likely.

function Random.Pick<T>(list: { T }): T

Returns

T - One of them.

Throws when the list is empty.

[Server]

Reorders a list, every ordering equally likely.

function Random.Shuffle<T>(list: { T }): { T }

Returns

{ T } - That same list, reordered.

Throws when given something other than a table.

This reorders in place. The list handed in is the list handed back, so anything else holding it sees the new order too. Shuffling a shared loot table rearranges it for every later reader. Copy it first when the original order matters:

local order = Random.Shuffle(table.clone(LOOT))

[Server]

Returns text of the given length that nobody can guess ahead of time.

function Random.Id(length: number): string

Returns

string - Text of that length, drawn from an alphabet of 62 letters and digits.

Throws when the length is not a whole number above zero.

Length is the only thing that decides how hard it is to guess. Use it for redeem codes, session ids, and the Token secret.

Twill.Token.Configure({ Secret = Random.Id(64) })

[Server]

Opens a round whose commitment can be published before anything is drawn.

function Random.Commit(): Round
export type Round = {
Commitment: string,
Int: (self: Round, min: number, max: number) -> number,
NextNumber: (self: Round) -> number,
Pick: <T>(self: Round, list: { T }) -> T,
Reveal: (self: Round) -> string,
}

Returns

Round - A round, with its Commitment ready to publish.

Publish the commitment first, draw, then reveal. In that order it proves something; in any other order it proves nothing.

[Server]

Reopens a revealed round so its draws can be taken again.

function Random.Replay(seed: string): Round

Returns

Round - A round that draws exactly what the original drew.

Throws when the seed is not hexadecimal, or not 32 bytes of it.

[Server]

Reports whether a revealed seed is the one a commitment was made to.

function Random.Verify(commitment: string, seed: string): boolean

Returns

boolean - true only when the seed matches the commitment.

Anything malformed answers false rather than raising, because this reads untrusted input: the point is that a player can hand you a seed to check.

The verifyroll command puts this in the admin console, so a moderator settles a dispute in front of the player without either of them having to trust the other.

[Server]

Returns a whole number between the bounds, taken from this round’s stream.

function round:Int(min: number, max: number): number

Returns

number - A value between the bounds, both reachable.

Throws on the same bounds Random.Int refuses.

[Server]

Returns a fraction of one, taken from this round’s stream.

function round:NextNumber(): number

Returns

number - A value from zero, up to but never reaching one.

Named to match the method an ordinary Random carries, so anything drawing from one draws from a round without knowing which it was handed. Chance is what this exists for.

Two words of the stream are taken per call rather than one, giving 53 bits of fraction. A round drawing fractions therefore consumes its stream twice as fast as one drawing whole numbers, which matters only when replaying an interleaved sequence.

[Server]

Returns one entry of a list, taken from this round’s stream.

function round:Pick<T>(list: { T }): T

Returns

T - One of them, every entry equally likely.

Throws when the list is empty.

[Server]

Hands back the seed so anyone can check the commitment and replay the draws.

function round:Reveal(): string

Returns

string - The seed as hexadecimal, safe to publish as text.

Reveal only after the round is over. Revealing it earlier tells everybody what is coming, because the stream is fixed by the seed.

LimitValue
Seed32 bytes
Revealed seed as text64 hexadecimal characters
Widest span Int draws2^32 values
Stream block64 bytes
Id alphabet62 characters
NextNumber precision53 bits