Skip to content
DiceSim

Tools and scripting

How to write a dice bot script in Lua

A dice bot script is a short Lua program that decides the next bet after every roll. This tutorial shows how to write a dice bot script in Lua for DiceSim, starting from an empty skeleton and ending with a capped martingale that has a stop loss, locks profit into the vault and exposes its settings to the Auto-Tuner.

Each sample on this page is loaded into the DiceSim engine and run for 2,000 rolls as part of the site’s automated tests. Copy any of them into the simulator to try it.

How a dice bot script runs

A DiceSim script has two parts, and the engine treats them differently.

  1. The top level, everything outside a function, runs once before the first roll. This is where you set the win chance, the side and the first stake.
  2. The dobet() function runs after every roll. By the time it is called, the engine has settled the bet and updated win, balance, currentstreak and the other session variables.

When dobet() returns, the engine reads nextbet, chance and bethigh and places the next bet with them. A script never places bets itself. It only sets those three values and lets the loop do the rest.

luaskeleton.lua
-- Top level: runs once, before the first roll
chance  = 49.5            -- win chance in percent (2.00x payout at a 1% edge)
bethigh = false           -- bet under: a roll below 49.5 wins
basebet = balance / 1000  -- one unit = 0.1% of the starting balance
nextbet = basebet         -- the size of the first bet

-- dobet(): runs after every roll, with win, balance and friends updated
function dobet()
  -- decide nextbet (and optionally chance, bethigh) for the next roll
end

Step 1: top-level setup

Four variables cover almost every script’s setup.

  • chance is the win chance in percent, from 0.01 to 98. The payout follows from it: (100 - houseedge) / chance, so 49.5 pays 2.00x at a 1% edge.
  • bethigh picks the side. false wins when the roll is below chance, true wins when it is above 100 - chance. Both sides have the same odds.
  • basebet is your unit. Setting it as a fraction of balance, such as balance / 1000, keeps the script independent of currency and bankroll size.
  • nextbet is the stake of the next roll. At the top level it sets the very first bet.

Local variables declared at the top level with local keep their values between calls to dobet(), which makes them the place to store counters and targets.

Step 2: a first flat bot

The simplest working bot bets the same amount every roll. It is a useful baseline: whatever a cleverer script does, compare it with this one on the same seeds.

luaflat-bot.lua
chance  = 49.5
bethigh = false
basebet = balance / 1000
nextbet = basebet

function dobet()
  nextbet = basebet   -- same stake every roll

  -- a progress line every 500 bets
  if bets % 500 == 0 then
    print(bets .. " bets, balance " .. string.format("%.8f", balance))
  end
end

bets % 500 == 0 is true on every 500th bet, so the log gets a progress line without one entry per roll. The log keeps at most 500 lines per batch, so printing on every roll would hide the lines you care about.

Step 3: reading the result of each roll

Inside dobet() these read-only variables describe what just happened. Assigning to them changes nothing in the engine, and the editor warns if you try.

Session variables most dice bot scripts read
VariableMeaning
wintrue if the roll that just finished won
previousbetstake of the roll that just finished
currentstreakwins in a row as a positive number, losses in a row as a negative number
currentprofitprofit or loss of the last roll alone
balancecurrent balance, not counting vaulted funds
profitbalance plus vaulted minus the starting balance
lastrollresult of the last roll, 0.00 to 100.00
bets, wins, lossescounts for the session
houseedgehouse edge in percent for this run
luareading-state.lua
chance  = 49.5
basebet = balance / 1000
nextbet = basebet

function dobet()
  if win then
    -- currentprofit is what the last roll alone made or lost
    print("win on " .. lastroll .. ", roll profit " .. string.format("%.8f", currentprofit))
  end

  -- currentstreak is negative while losing: -6 means six losses in a row
  if currentstreak == -6 then
    print("6 losses in a row, last stake " .. string.format("%.8f", previousbet))
  end

  nextbet = basebet
end

currentstreak does most of the work in streak-based strategies. A check such as currentstreak <= -5 is true from the fifth loss in a row onwards, and currentstreak == -6 is true only on the sixth.

Step 4: a martingale with a loss cap

Martingale doubles the stake after each loss and goes back to one unit after a win. At a 2.00x payout every win ends the cycle one unit ahead. The cost is the size of the bets late in a losing run: after 10 losses in a row the stakes add up to 1 + 2 + 4 + … + 512 = 1,023 units.

A plain martingale keeps doubling until a stake no longer fits the balance and the run ends as bankrupt. The version below caps the cycle at 10 losses, takes the loss and starts again from one unit. With basebet = balance / 2048, one lost cycle costs about half the starting balance.

luamartingale-capped.lua
chance  = 49.5
bethigh = false
basebet = balance / 2048  -- a full 10-step cycle costs 1023 units, about half the balance
nextbet = basebet

local maxLosses = 10      -- give up on a cycle after this many losses
local lossRun   = 0

function dobet()
  if win then
    lossRun = 0
    nextbet = basebet
    return
  end

  lossRun = lossRun + 1
  if lossRun >= maxLosses then
    -- accept the loss of this cycle and start again from one unit
    lossRun = 0
    nextbet = basebet
  else
    nextbet = previousbet * 2
  end
end

The counter lossRun is used instead of currentstreak because the streak keeps counting after the reset, while the cycle should start over. The dice calculator shows how often a 10-loss streak turns up at 49.5% and what it costs for any base bet. The ready-made martingale script has an uncapped version you can open in the simulator.

Step 5: stop(), vault() and a stop loss

Three functions control a run from inside the script.

  • stop() ends the run after the current roll.
  • vault(amount) moves money out of balance into the vault. Vaulted funds count toward profit but can never be bet again.
  • print(...) writes a line to the log panel.
luastop-and-vault.lua
chance  = 49.5
bethigh = false
basebet = balance / 2048
nextbet = basebet

local bankroll   = balance          -- remember what we started with
local checkpoint = balance          -- balance after the last vault
local takeStep   = bankroll * 0.05  -- vault every 5% gain
local stopLoss   = bankroll * 0.25  -- quit after losing 25% overall

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

  -- lock gains away: vaulted coins count as profit and can never be bet
  if balance - checkpoint >= takeStep then
    vault(balance - checkpoint)
    checkpoint = balance
  end

  -- profit = balance + vaulted - starting balance
  if profit <= -stopLoss then
    print("stop loss reached after " .. bets .. " bets")
    stop()
    return
  end

  -- never ask for more than we hold, or the run ends as bankrupt
  if nextbet > balance then
    nextbet = basebet
  end
end

This script vaults every 5% gain over the last checkpoint and stops once overall profit, vault included, is a loss of 25% of the starting balance. The last check falls back to one unit whenever the doubled stake would be larger than the balance, so the run never ends as bankrupt while it can still afford a base bet.

Step 6: make it tunable with params

Hard-coded numbers are fine for a first version, but finding good values means editing and rerunning by hand. DiceSim scripts can read settings from the read-only params table instead, always with a default after or:

luatunable.lua
-- Every setting reads params.<name> first, so the Auto-Tuner can try other values
chance  = params.chance or 49.5
bethigh = false
basebet = params.basebet or balance / 2048
nextbet = basebet

local multiplier = params.multiplier or 2    -- stake growth after a loss
local maxLosses  = params.maxlosses or 10    -- cycle length before giving up
local lossRun    = 0

function dobet()
  if win then
    lossRun = 0
    nextbet = basebet
    return
  end

  lossRun = lossRun + 1
  if lossRun >= maxLosses then
    lossRun = 0
    nextbet = basebet
  else
    nextbet = previousbet * multiplier
  end

  if nextbet > balance then
    stop()
  end
end

Run on its own, the script uses the defaults. The Auto-Tuner in the simulator finds every params.name in the code and lets you give up to three of them a range. It then runs the script for each combination, either as a full grid or with a genetic search, and ranks the results by profit divided by drawdown, weighted by how many rolls the run survived. Applying a row writes the chosen values back into the script as defaults.

Common mistakes

A nextbet larger than balance

If nextbet is larger than balance when the next bet is due, the engine ends the run as bankrupt without placing it. Progressions hit this sooner than expected. Compare the next stake with balance and either reset to basebet or call stop(), as the stop-and-vault script above does.

A chance outside 0.01 to 98

chance must stay between 0.01 and 98. A value outside that range raises a runtime error that halts the run and names the line. Scripts that change the chance on the fly should clamp it with math.max and math.min.

luasafe-chance.lua
chance  = 49.5
basebet = balance / 1000
nextbet = basebet

function dobet()
  -- lower the chance by 5 points after each loss, but stay inside 0.01 to 98
  if win then
    chance = 49.5
  else
    chance = math.max(0.01, chance - 5)
  end
  nextbet = basebet
end

A missing end

Every function, if, for and while block in Lua closes with its own end. An if ... else ... end inside dobet() therefore needs two end lines in total: one for the if and one for the function. A missing one is reported as a syntax error when the script loads, usually pointing at the end of the file.

Using math.random

math.random works, but it is not seeded from the provably fair seeds. A run that uses it gives different results each time on the same seeds, so it cannot be replayed or verified for the leaderboard. Derive any randomness from lastroll or nonce instead, both of which come from the seeds:

luanonce-switch.lua
chance  = 49.5
basebet = balance / 1000
nextbet = basebet

function dobet()
  -- a reproducible "coin flip" for the side: derived from the last roll,
  -- which comes from the provably fair seeds (math.random does not)
  bethigh = math.floor(lastroll * 100) % 2 == 0
  nextbet = basebet
end

Assigning to read-only variables

Lines such as balance = 1 or win = true have no effect on the run, because the engine refreshes them before every call to dobet(). Steer the bot through nextbet, chance and bethigh, which the engine reads back after each call.

Testing your bot

Run the script in the simulator for a few hundred thousand rolls and watch the balance chart, the maximum drawdown and the longest losing streak. One run is one draw of luck, so use the stress test to repeat the same script on five different seed pairs and see how far the outcomes spread. Reading simulation results explains each number.

Every global and function is listed with an example in the Lua reference. The free dice bot scripts are complete, commented strategies to read, run and modify.

FAQ

What language are dice bot scripts written in?

Lua. DiceSim runs Lua 5.4 with DiceBot-style globals such as chance, nextbet, previousbet, win and dobet(), so many existing scripts run with few or no changes.

Why did my script stop with bankrupt?

The script asked for a nextbet larger than balance. The engine ends the run before placing that bet. Cap the stake, reset to basebet, or call stop() when the next bet would not fit.

Can I use math.random in a dice bot?

You can, but math.random is not seeded from the provably fair seeds, so the run will not replay the same way and cannot be verified for the leaderboard. Derive any randomness from lastroll or nonce instead.

What does params do?

params is a read-only table of values passed in from outside the script. Writing params.multiplier or 2 lets the Auto-Tuner try other values while the script still runs on its own with the default.

Is a bot that wins in the simulator profitable at a casino?

Not on average. Every bet has the same negative expected value whatever the script does. A simulation shows how a strategy behaves, how deep its drawdowns go and how often it busts.