Run a draw a player can audit
The moment a draw decides something a player paid for, two claims are being made: that the odds are what you said, and that you did not reroll a result you did not like. Neither is visible from the outside, and “trust us” stops working at exactly the point it starts mattering.
A commitment turns the second claim into something anyone can check, without seeing your code and without taking your word for it.
Use the right generator
Section titled “Use the right generator”local prize = Twill.Random.Pick(lootTable)local code = Twill.Random.Id(16)Every draw comes from a cryptographic generator, so knowing every earlier result says nothing about the next one.
math.random does not have that property. A player who watches enough outcomes
can predict the rest, and for a crate opening that is worth real money, some of
them will.
Reach for Twill.Random for anything a player would care about the outcome of.
math.random is fine for cosmetic variety.
Server only, because an outcome a client can draw is an outcome it can choose.
Publish a commitment first
Section titled “Publish a commitment first”A commitment proves the result was fixed before it was announced.
function CrateService.OpenCrate(player) local round = Twill.Random.Commit()
-- Published before anything is drawn. This is the whole proof: the seed -- behind it already existed, so the outcome cannot be chosen afterwards. Twill.Replication.SetFor(player, "Crate", { Commitment = round.Commitment, })
local prize = round:Pick(lootTable) grant(player, prize)
-- Only now is the seed safe to publish. Revealing it any earlier tells -- the player what is coming. Twill.Replication.SetFor(player, "Crate", { Commitment = round.Commitment, Prize = prize, Seed = round:Reveal(), })endThe player receives the commitment before anything is drawn, and the seed afterwards. Anyone can then check that the seed produces that commitment, and replay the draw.
Let them check it
Section titled “Let them check it”local matches = Twill.Random.Verify(commitment, seed)
local replay = Twill.Random.Replay(seed)local prize = replay:Pick(lootTable)Draws are keyed Blake3 over the seed, so any implementation of Blake3 reproduces them. A player does not have to trust your code, or run it.
Publish the loot table too
Section titled “Publish the loot table too”A commitment proves the draw was not rerolled. It proves nothing about what was in the list.
If the point is to be auditable, the table has to be visible and stable. Publish it, version it, and do not change it mid-round.
Generate a secret
Section titled “Generate a secret”Twill.Token.Configure({ Secret = Twill.Random.Id(64) })Ranges
Section titled “Ranges”local roll = Twill.Random.Int(1, 100)Inclusive at both ends. Random.Int(5, 5) is legal and returns 5.
The span must fit in 32 bits. That is far past any loot table, and it is what keeps the draw unbiased, which a wider span could not be from a single word.
Weighted odds
Section titled “Weighted odds”Chance holds tables where some entries are likelier than
others, and it takes a round directly, so a weighted draw is audited exactly the
way an even one is.
local round = Twill.Random.Commit()announce(round.Commitment)
local pool = Twill.Chance.new(round)pool:AddItem("common", 100)pool:AddItem("legendary", 1, 2)
local prize = pool:Next(playerLuck)
announce(round:Reveal())Random.Replay(seed) then reproduces that prize, as long as the draws are taken
in the same order.
Where the draw is sold, the odds have to be disclosed before the purchase, and
so does the effect of anything sold that improves them. Pool:GetProbabilities
answers both, at any luck.