Skip to content
DiceSim

Module 2

Lua strategy reference

Strategies are plain Lua 5.4 with a handful of globals injected by the engine. The names match Seuntjie’s DiceBot, so most existing scripts run unchanged. This page lists each global with a minimal example, then explains the engine rules and how each roll is derived from the seeds.

Execution model

  1. The engine compiles your script and runs its top level once. Set chance, basebet and nextbet there.
  2. 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 calls dobet().
  3. After dobet() returns it reads nextbet, chance and bethigh for the next roll. A nextbet larger than balance ends the run as bankrupt.
  4. 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.
luaAnatomy of a strategy
-- 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
end

Control 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/write

Win 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.

lua
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
end

nextbet

number read/write

Amount 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.

lua
basebet = balance / 1000
nextbet = basebet
function dobet()
  if win then nextbet = basebet else nextbet = previousbet * 2 end
end

bethigh

boolean read/write

true = bet over (roll > 100 - chance), false = bet under (roll < chance). Flip it to alternate sides.

lua
function dobet()
  if currentstreak <= -3 then bethigh = not bethigh end   -- flip side after 3 losses
end

currency

string read/write

Currency code of the session ("btc", "eth", …). Informational in the simulator.

lua
print("running in", currency)   -- "btc", informational only

basebet

number read/write

Convenience 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.

lua
basebet = Units.FromSats(100)   -- 100 sats
nextbet = basebet
function dobet()
  if win then nextbet = basebet end
end

Session 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-only

Current balance (excludes vaulted funds). Updated after every roll.

lua
function dobet()
  if balance < basebet * 50 then stop() end   -- bail out when thin
end

profit

number read-onlyread-only

Session profit: balance + vaulted - starting balance. resetstats() re-bases it to 0.

lua
function dobet()
  if profit >= 0.001 then vault(profit); resetstats() end
  if profit <= -0.002 then stop() end
end

wagered

number read-onlyread-only

Total amount wagered this session.

lua
function dobet()
  if wagered >= 1.0 then print("1 BTC turnover reached"); stop() end
end

win

boolean read-onlyread-only

true if the last roll won. The first thing most dobet() bodies check.

lua
function dobet()
  if win then nextbet = basebet else nextbet = previousbet * 2 end
end

lastBet

number read-onlyread-only

Amount of the last bet placed (alias of previousbet).

lua
function dobet()
  if not win then nextbet = lastBet + basebet end   -- D'Alembert step
end

previousbet

number read-onlyread-only

Amount of the last bet placed. nextbet = previousbet * 2 is the martingale step.

lua
function dobet()
  if not win then nextbet = previousbet * 2 else nextbet = basebet end
end

currentprofit

number read-onlyread-only

Profit or loss of the last roll alone (positive on a win, -previousbet on a loss).

lua
function dobet()
  if currentprofit > 0 then print("won", currentprofit) end
end

currentstreak

integer read-onlyread-only

Consecutive results: positive = wins in a row, negative = losses in a row.

lua
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
end

bets

integer read-onlyread-only

Number of bets placed this session.

lua
function dobet()
  if bets % 10000 == 0 then print(bets, balance) end
end

wins

integer read-onlyread-only

Number of winning bets this session.

lua
function dobet()
  if bets > 0 then local wr = wins / bets end
end

losses

integer read-onlyread-only

Number of losing bets this session.

lua
function dobet()
  if losses > wins * 1.2 then chance = 66 end   -- adapt when running cold
end

vaulted

number read-onlyread-only

Total moved to the vault with vault(amount). Counts toward profit, cannot be bet.

lua
function dobet()
  if vaulted >= 0.01 then stop() end   -- target locked in
end

nonce

integer read-onlyread-only

Provably-fair nonce of the next bet. Increments every roll, resets to 0 on resetseed().

lua
function dobet()
  if nonce >= 100000 then resetseed() end   -- rotate seed every 100k bets
end

lastroll

number read-onlyread-only

Result of the last roll (0.00 to 100.00).

lua
function dobet()
  if lastroll > 99 then print("99+ roll at bet", bets) end
end

houseedge

number read-onlyread-only

House edge in percent for this run (e.g. 1). Payout = (100 - houseedge) / chance.

lua
local payout = (100 - houseedge) / chance
function dobet()
  nextbet = win and basebet or previousbet * payout / (payout - 1)   -- exact recovery
end

params

table read-onlyread-only

Parameters injected by the grid search / auto-tuner (SRS §2.14) or the run config. Read with a default: local mult = params.multiplier or 2.

lua
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
end

Functions

dobet

function dobet() (required)function

Your strategy. Called after every roll with the session variables updated. Set nextbet, chance, bethigh for the next roll.

lua
function dobet()
  -- runs after EVERY roll; set nextbet / chance / bethigh for the next one
end

vault

vault(amount: number): numberfunction

Move 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.

lua
function dobet()
  if profit > 0.0005 then
    vault(profit)      -- move profit out of reach of the bets
    resetstats()
  end
end

resetseed

resetseed()function

Rotate to a fresh random server seed and reset the nonce to 0 after the current roll. In replay mode it is a no-op.

lua
function dobet()
  if currentstreak <= -10 then resetseed() end   -- superstition, but reproducible
end

resetstats

resetstats()function

Zero bets, wins, losses, wagered, streaks and drawdown, and re-base profit to 0 from the current equity. Balance is untouched.

lua
function dobet()
  if bets % 50000 == 0 then resetstats() end   -- measure in 50k-roll windows
end

stop

stop()function

End the run after the current roll (halt reason stop).

lua
function dobet()
  if balance >= 0.02 or balance < 0.005 then stop() end
end

print

print(...)function

Write to the simulator log panel (tab-separated). Capped at 500 lines per batch, so log on events instead of every roll.

lua
function dobet()
  if currentstreak <= -9 then print(string.format("streak %d  bet %.8f", currentstreak, nextbet)) end
end

Units.ToSats

Units.ToSats(x: number): numberfunction

Convert a native amount to satoshis (× 1e8).

lua
print(Units.ToSats(0.00012345))   -- 12345

Units.FromSats

Units.FromSats(x: number): numberfunction

Convert satoshis to a native amount (÷ 1e8).

lua
basebet = Units.FromSats(250)     -- 0.0000025

Standard 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.

Lua standard library functions available to strategies
SignatureNotes
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. string.format("%.8f", balance).

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 is bet × (multiplier − 1). A loss costs the bet.
  • Win rule. bethigh = false wins when roll < chance; bethigh = true wins when roll > 100 − chance. Both sides have identical odds.
  • Bankruptcy. If nextbet > balance when a bet is placed, the run halts with wasBankrupt = true. Vaulted funds are never bet.
  • Drawdown. maxDrawdown is the largest drop of equity (balance + vaulted) from its running peak, reported as a negative number.
  • Validation. nextbet must be finite and ≥ 0; chance must 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.

  1. 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.
  2. Take the first four bytes b0 b1 b2 b3 of the 64-byte digest and turn them into a number in [0, 1): float = b0/256 + b1/256² + b2/256³ + b3/256⁴.
  3. Scale to the dice range: roll = floor(float × 10001) / 100, giving 0.00 to 100.00 inclusive with two decimals.
  4. 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.

tsReference implementation (TypeScript, @noble/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

luaMartingale with a loss cap
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
end
luaParoli (positive progression)
chance  = 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
end
luaRecovery sized from the real payout
chance  = 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
end
luaTake profit into the vault
chance  = 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
end

Ready-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.