Module 2
Lua strategy reference
Execution model
- The engine compiles your script and runs its top level once. Set
chance,basebetandnextbetthere. - For every roll it draws a number from 0.00 to 100.00 (see provably-fair rolls), decides win or loss, settles
balance, updates the session variables and then callsdobet(). - After
dobet()returns it readsnextbet,chanceandbethighfor the next roll. Anextbetlarger thanbalanceends the run as bankrupt. - The loop runs inside the Lua VM in batches of thousands of rolls, so a strategy executes at > 1,000,000 rolls/s in the browser. Anonymous sessions may run 100,000 rolls per batch, accounts 10,000,000, and the server-side agent API 1,000,000.
-- 1. top level: runs once -------------------------------------
chance = 49.5
basebet = balance / 2000
nextbet = basebet
local target = balance * 1.5
-- 2. dobet(): runs after every roll ------------------------------
function dobet()
if win then
nextbet = basebet
else
nextbet = previousbet * 2
end
if balance >= target then
vault(balance - basebet * 100) -- lock the win
stop()
end
endControl variables
Read/write globals the engine consumes before each roll. chance must stay between 0.01 and 98; out-of-range values raise a runtime error so the mistake is visible instead of silently clamped.
chance
number (0.01 to 98) read/writeWin chance in percent for the next bet. Payout multiplier is (100 - houseedge) / chance.
bethigh = false wins when roll < chance; bethigh = true wins when roll > 100 - chance.
chance = 49.5 -- 2.00x at 1% edge
function dobet()
-- switch to a long shot after 5 straight wins
if currentstreak >= 5 then chance = 9.9 else chance = 49.5 end
endnextbet
number read/writeAmount to wager on the next roll, in the native currency unit. Must be finite and ≥ 0. A nextbet larger than balance ends the run as bankrupt before the bet is placed.
basebet = balance / 1000
nextbet = basebet
function dobet()
if win then nextbet = basebet else nextbet = previousbet * 2 end
endbethigh
boolean read/writetrue = bet over (roll > 100 - chance), false = bet under (roll < chance). Flip it to alternate sides.
function dobet()
if currentstreak <= -3 then bethigh = not bethigh end -- flip side after 3 losses
endcurrency
string read/writeCurrency code of the session ("btc", "eth", …). Informational in the simulator.
print("running in", currency) -- "btc", informational onlybasebet
number read/writeConvenience variable: your base bet. Pre-filled with the initial nextbet; most templates set it at the top level and reset nextbet = basebet after a win.
basebet = Units.FromSats(100) -- 100 sats
nextbet = basebet
function dobet()
if win then nextbet = basebet end
endSession variables
Read-only state, refreshed after every roll before dobet() runs. Assigning to them has no effect on the engine and the editor’s linter warns about it.
balance
number read-onlyread-onlyCurrent balance (excludes vaulted funds). Updated after every roll.
function dobet()
if balance < basebet * 50 then stop() end -- bail out when thin
endprofit
number read-onlyread-onlySession profit: balance + vaulted - starting balance. resetstats() re-bases it to 0.
function dobet()
if profit >= 0.001 then vault(profit); resetstats() end
if profit <= -0.002 then stop() end
endwagered
number read-onlyread-onlyTotal amount wagered this session.
function dobet()
if wagered >= 1.0 then print("1 BTC turnover reached"); stop() end
endwin
boolean read-onlyread-onlytrue if the last roll won. The first thing most dobet() bodies check.
function dobet()
if win then nextbet = basebet else nextbet = previousbet * 2 end
endlastBet
number read-onlyread-onlyAmount of the last bet placed (alias of previousbet).
function dobet()
if not win then nextbet = lastBet + basebet end -- D'Alembert step
endpreviousbet
number read-onlyread-onlyAmount of the last bet placed. nextbet = previousbet * 2 is the martingale step.
function dobet()
if not win then nextbet = previousbet * 2 else nextbet = basebet end
endcurrentprofit
number read-onlyread-onlyProfit or loss of the last roll alone (positive on a win, -previousbet on a loss).
function dobet()
if currentprofit > 0 then print("won", currentprofit) end
endcurrentstreak
integer read-onlyread-onlyConsecutive results: positive = wins in a row, negative = losses in a row.
function dobet()
if currentstreak <= -8 then nextbet = basebet end -- give up on the recovery
if currentstreak >= 3 then nextbet = basebet end -- Paroli: cash out after 3 wins
endbets
integer read-onlyread-onlyNumber of bets placed this session.
function dobet()
if bets % 10000 == 0 then print(bets, balance) end
endwins
integer read-onlyread-onlyNumber of winning bets this session.
function dobet()
if bets > 0 then local wr = wins / bets end
endlosses
integer read-onlyread-onlyNumber of losing bets this session.
function dobet()
if losses > wins * 1.2 then chance = 66 end -- adapt when running cold
endvaulted
number read-onlyread-onlyTotal moved to the vault with vault(amount). Counts toward profit, cannot be bet.
function dobet()
if vaulted >= 0.01 then stop() end -- target locked in
endnonce
integer read-onlyread-onlyProvably-fair nonce of the next bet. Increments every roll, resets to 0 on resetseed().
function dobet()
if nonce >= 100000 then resetseed() end -- rotate seed every 100k bets
endlastroll
number read-onlyread-onlyResult of the last roll (0.00 to 100.00).
function dobet()
if lastroll > 99 then print("99+ roll at bet", bets) end
endhouseedge
number read-onlyread-onlyHouse edge in percent for this run (e.g. 1). Payout = (100 - houseedge) / chance.
local payout = (100 - houseedge) / chance
function dobet()
nextbet = win and basebet or previousbet * payout / (payout - 1) -- exact recovery
endparams
table read-onlyread-onlyParameters injected by the grid search / auto-tuner (SRS §2.14) or the run config. Read with a default: local mult = params.multiplier or 2.
local mult = params.multiplier or 2 -- supplied by grid search / agent API
function dobet()
if not win then nextbet = previousbet * mult else nextbet = basebet end
endFunctions
dobet
function dobet() (required)functionYour strategy. Called after every roll with the session variables updated. Set nextbet, chance, bethigh for the next roll.
function dobet()
-- runs after EVERY roll; set nextbet / chance / bethigh for the next one
endvault
vault(amount: number): numberfunctionMove amount from balance to the vault (clamped to the available balance). Vaulted funds count as profit but can never be lost. Returns the amount moved.
function dobet()
if profit > 0.0005 then
vault(profit) -- move profit out of reach of the bets
resetstats()
end
endresetseed
resetseed()functionRotate to a fresh random server seed and reset the nonce to 0 after the current roll. In replay mode it is a no-op.
function dobet()
if currentstreak <= -10 then resetseed() end -- superstition, but reproducible
endresetstats
resetstats()functionZero bets, wins, losses, wagered, streaks and drawdown, and re-base profit to 0 from the current equity. Balance is untouched.
function dobet()
if bets % 50000 == 0 then resetstats() end -- measure in 50k-roll windows
endstop
stop()functionEnd the run after the current roll (halt reason stop).
function dobet()
if balance >= 0.02 or balance < 0.005 then stop() end
endWrite to the simulator log panel (tab-separated). Capped at 500 lines per batch, so log on events instead of every roll.
function dobet()
if currentstreak <= -9 then print(string.format("streak %d bet %.8f", currentstreak, nextbet)) end
endUnits.ToSats
Units.ToSats(x: number): numberfunctionConvert a native amount to satoshis (× 1e8).
print(Units.ToSats(0.00012345)) -- 12345Units.FromSats
Units.FromSats(x: number): numberfunctionConvert satoshis to a native amount (÷ 1e8).
basebet = Units.FromSats(250) -- 0.0000025Standard library
The safe parts of the Lua standard library are available: math, string, table, tostring, tonumber, pairs/ipairs, type. os, io, require, load and debug are removed from the sandbox.
| Signature | Notes |
|---|---|
| math.floor(x) | Largest integer ≤ x. |
| math.ceil(x) | Smallest integer ≥ x. |
| math.max(a, b, ...) | Largest argument. |
| math.min(a, b, ...) | Smallest argument. |
| math.abs(x) | Absolute value. |
| math.random([m [, n]]) | Pseudo-random number. Note: not derived from the provably-fair seeds, so runs using it are not reproducible. |
| number | Positive infinity. |
| string.format(fmt, ...) | C-style formatting, e.g. |
| table.insert(t, [pos,] value) | Append or insert into a table. |
| table.remove(t [, pos]) | Remove and return an element (last by default). |
| tostring(v) | Convert any value to a string. |
| tonumber(v) | Convert a string to a number (nil if impossible). |
Engine rules
- Payout. A win returns
bet × (100 − houseedge) / chance; profit per win isbet × (multiplier − 1). A loss costs the bet. - Win rule.
bethigh = falsewins whenroll < chance;bethigh = truewins whenroll > 100 − chance. Both sides have identical odds. - Bankruptcy. If
nextbet > balancewhen a bet is placed, the run halts withwasBankrupt = true. Vaulted funds are never bet. - Drawdown.
maxDrawdownis the largest drop of equity (balance + vaulted) from its running peak, reported as a negative number. - Validation.
nextbetmust be finite and ≥ 0;chancemust be between 0.01 and 98. Violations raise a Lua error that halts the run with the offending line number. - Time budget. A
dobet()that never returns is killed by the batch time budget and reported as an error. - Logs.
print()output is capped per batch; log on events, not every roll.
Provably-fair rolls
Every roll is a deterministic function of three values: a server seed (secret until revealed), a client seed (chosen by the player) and a nonce that counts bets. DiceSim implements the scheme Stake publishes, so a seed pair copied from a casino replays bet-for-bet.
- Compute
HMAC-SHA512(key = serverSeed, message = clientSeed:nonce:cursor). The message is the three values joined with colons, e.g.my-client-seed:41207:0. - Take the first four bytes
b0 b1 b2 b3of the 64-byte digest and turn them into a number in [0, 1):float = b0/256 + b1/256² + b2/256³ + b3/256⁴. - Scale to the dice range:
roll = floor(float × 10001) / 100, giving 0.00 to 100.00 inclusive with two decimals. - Decide the outcome with the win rule above, settle the bet, then increment the nonce. The next bet repeats from step 1.
The cursor is 0 for the default one-roll-per-nonce mode. The engine also offers a throughput mode that draws eight rolls from one digest (bytes 0 to 3, then 4 to 7, and so on through 28 to 31) and increments the cursor every eight rolls, trading casino compatibility for 8× fewer hashes.
import { hmac } from "@noble/hashes/hmac";
import { sha512 } from "@noble/hashes/sha2";
export function roll(serverSeed: string, clientSeed: string, nonce: number, cursor = 0): number {
const msg = new TextEncoder().encode(`${clientSeed}:${nonce}:${cursor}`);
const h = hmac(sha512, new TextEncoder().encode(serverSeed), msg);
const float = h[0] / 256 + h[1] / 65536 + h[2] / 16777216 + h[3] / 4294967296;
return Math.floor(float * 10001) / 100; // 0.00 … 100.00
}
export function wins(roll: number, chance: number, betHigh: boolean): boolean {
return betHigh ? roll > 100 - chance : roll < chance;
}Inside a strategy, nonce is readable and resetseed() rotates to a fresh random server seed and resets the nonce to 0. Because the sequence is fully determined by the seeds, two runs with the same seeds and the same script produce byte-identical results. The leaderboard’s server-side verification relies on this.
Common patterns
chance = 49.5
basebet = balance / 4096 -- 12 doublings fit
nextbet = basebet
function dobet()
if win then
nextbet = basebet
elseif currentstreak <= -12 then
nextbet = basebet -- accept the loss, don't bust
else
nextbet = previousbet * 2
end
endchance = 49.5
basebet = balance / 500
nextbet = basebet
function dobet()
if win and currentstreak < 3 then
nextbet = previousbet * 2 -- press the win
else
nextbet = basebet -- reset after 3 wins or any loss
end
endchance = 33
basebet = balance / 5000
nextbet = basebet
local payout = (100 - houseedge) / chance
local lost = 0
function dobet()
if win then
lost = 0
nextbet = basebet
else
lost = lost + previousbet
-- bet exactly enough to recover everything lost plus one base unit
nextbet = (lost + basebet) / (payout - 1)
end
endchance = 49.5
basebet = balance / 1000
nextbet = basebet
local start = balance
function dobet()
if win then nextbet = basebet else nextbet = previousbet * 2 end
if balance > start * 1.10 then
vault(balance - start) -- bank the 10%
resetstats()
start = balance
end
endReady-made versions of these live in the simulator’s template menu. Before running any of them for real, put the same numbers into the calculator. It shows how many losses in a row the bankroll survives and how often that streak shows up.