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"
    }
}

