This is a wrapper I wrote for math.random(). I have a larger program that will create a JSON file to 'load' from when I want to resume. However my program heavily used math.random() which made it hard to debug and test the program because different results would occur depending on if the program was started fresh, or from a load file.
This wrapper lets you "resume" from a random seed.
RandomNumber.lua
--[[
RandomNumber - Lua Class for Generating and Managing Random Numbers
The RandomNumber class is designed to facilitate the generation of random numbers
using Lua's built-in math.random function. It includes features for tracking the
number of generated random numbers and the ability to 'jump' to a specific iteration.
This functionality is particularly useful for debugging and testing. Example if
a program is to save its state and 'resume' and we want numbers generated to not
reset,
Usage:
local rng = RandomNumber:new(seed) -- Instantiate class, 'seed' is optional but suggested
rng:jumpToIteration(10) -- Jump to the 10th iteration. Next generate() method will be the 11th
rng:generate() -- generate a random number
--]]
---@class RandomNumber
local RandomNumber = {}
-- Wrapper for math.random() in case we want to change in the future
local function generateRandomNumber(a, b)
if a and b then
return math.random(a, b)
elseif a then
return math.random(a)
end
return math.random()
end
---@return RandomNumber
function RandomNumber:new(seed)
---@type RandomNumber
local randomNumber = {}
self = self or randomNumber
self.__index = self
setmetatable(randomNumber, self)
randomNumber.count = 0
if seed then
math.randomseed(seed)
randomNumber.seed = seed
end
return randomNumber
end
-- Reset the RandomNumber object with a new seed.
---@param seed number
function RandomNumber:reset(seed)
math.randomseed(seed)
self.count = 0
self.seed = seed
end
function RandomNumber:generate(a, b)
self.count = self.count + 1
return generateRandomNumber(a, b)
end
-- Jumps to a point in the random number sequence.
-- Warning: Will produce new random seed if no seed was ever given
---@param iteration number
function RandomNumber:jumpToIteration(iteration)
if type(iteration) ~= "number" or iteration < 0 then
error("Invalid argument, expected a number 0 or greater: " .. iteration)
end
local difference = iteration - self.count
if difference > 0 then
for _ = 1, difference do
generateRandomNumber()
end
elseif difference < 0 then
self = RandomNumber:new(self.seed)
self:jumpToIteration(iteration)
end
self.count = iteration
end
return RandomNumber
RandomNumberTest.lua:
-- Import LuaUnit module
local lu = require('luaunit')
-- Import the RandomNumber class
local RandomNumber = require('RandomNumber')
-- Test the RandomNumber class
TestRandomNumber = {}
---@class RandomNumber
local rng
local firstNumberInSeq = '0.23145237586596'
local secondNumberInSeq = '0.58485671559801'
local hundredthNumberInSeq = '0.43037202063051'
function TestRandomNumber:setUp()
-- Set a fixed seed value for reproducibility in tests
rng = RandomNumber:new(12345)
end
-- Function to round a number to a specified decimal place
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
function TestRandomNumber:testGenerate()
-- use tostring method, otherwise the comparison fails between floats
-- Numbers are based on math.random(), given seed 12345
lu.assertEquals(tostring(rng:generate()), firstNumberInSeq)
lu.assertEquals(tostring(rng:generate()), secondNumberInSeq)
end
function TestRandomNumber:testJump()
rng:jumpToIteration(99)
lu.assertEquals(tostring(rng:generate()), hundredthNumberInSeq)
end
function TestRandomNumber:testGenerateAndJump()
for _=1, 50 do
rng:generate()
end
rng:jumpToIteration(99)
lu.assertEquals(tostring(rng:generate()), hundredthNumberInSeq)
end
function TestRandomNumber:testJumpBackwards()
for _=1, 50 do
rng:generate()
end
rng:jumpToIteration(1)
lu.assertEquals(tostring(rng:generate()), secondNumberInSeq)
end
function TestRandomNumber:testJumpZero()
for _=1, 99 do
rng:generate()
end
-- jump to the iteration 0
rng:jumpToIteration(0)
-- Should be 1st
lu.assertEquals(tostring(rng:generate()), firstNumberInSeq)
end
function TestRandomNumber:testJumpToSameSpot()
for _=1, 99 do
rng:generate()
end
rng:jumpToIteration(99)
lu.assertEquals(tostring(rng:generate()), hundredthNumberInSeq)
end
function TestRandomNumber:testReset()
rng:generate()
rng:jumpToIteration(105)
rng:reset(12345)
lu.assertEquals(tostring(rng:generate()), firstNumberInSeq)
lu.assertEquals(tostring(rng:generate()), secondNumberInSeq)
end
function TestRandomNumber:testJumpToIterationInvalidArgumentNegative()
-- Use pcall to catch the error
local success, errorMessage = pcall(function()
rng:jumpToIteration(-1)
end)
-- Check that pcall was not successful (error was thrown)
lu.assertFalse(success)
-- Check that the error message is as expected
lu.assertStrContains(errorMessage, "Invalid argument")
end
function TestRandomNumber:testJumpToIterationInvalidArgumentNaN()
-- Use pcall to catch the error
local success, errorMessage = pcall(function()
rng:jumpToIteration('11')
end)
-- Check that pcall was not successful (error was thrown)
lu.assertFalse(success)
-- Check that the error message is as expected
lu.assertStrContains(errorMessage, "Invalid argument")
end
function TestRandomNumber:testJumpToIterationNilSeed()
local rngTarget = RandomNumber:new()
lu.assertNil(rngTarget.seed)
rngTarget:generate()
rngTarget:generate()
rngTarget:jumpToIteration(0)
lu.assertNil(rngTarget.seed)
rngTarget:generate()
rngTarget:generate()
end
function TestRandomNumber:testJumpToIterationWithSeed()
local rngTarget = RandomNumber:new(1550)
lu.assertEquals(1550, rngTarget.seed)
local firstValue = rngTarget:generate()
local secondValue = rngTarget:generate()
rngTarget:jumpToIteration(0)
lu.assertEquals(1550, rngTarget.seed)
local newValue1 = rngTarget:generate()
local newValue2 = rngTarget:generate()
lu.assertEquals(firstValue, newValue1)
lu.assertEquals(secondValue, newValue2)
end
-- Run the tests
os.exit(lu.LuaUnit.run())
random-number-1.0.0-2.rockspec (Included in case suggestions can be made to it)
package = "random-number"
version = "1.0.0-2"
source = {
url = "git://github.com/LionelBergen/RandomNumber",
tag = "master",
}
description = {
summary = "RandomNumber generator and manager.",
detailed = [[
This is a wrapper for Lua's builtin math.random. Purpose is for debugging & testing applications
that use random numbers. Having numbers be reproduced the same way makes for easier testing & debugging
]],
homepage = "https://github.com/LionelBergen/RandomNumber",
}
dependencies = {
"lua >= 5.1",
}
build = {
["type"] = "builtin",
modules = {
randomnumber = "RandomNumber.lua"
}
}