uranium-core/stdlib/rng.lua

174 lines
4.1 KiB
Lua

-- xoshiro128** 1.1 by David Blackman and Sebastiano Vigna (vigna@acm.org) https://prng.di.unimi.it/xoshiro128starstar.c
-- Lua implementation by Jill "oatmealine" Monoids
-- Licensed under CC-BY-SA
local bitop = require('stdlib.bitop')
local RAND_MAX = 4294967295
---@param x int
---@param k int
local function rotl(x, k)
return bitop.bor(bitop.lshift(x, k), bitop.rshift(x, (32 - k)))
end
---@param state int[] @array of size 4; will be mutated
local function next(state)
local result = rotl(state[2] * 5, 7) * 9
local t = bitop.lshift(state[2], 9)
state[3] = bitop.bxor(state[3], state[1])
state[4] = bitop.bxor(state[4], state[2])
state[2] = bitop.bxor(state[2], state[3])
state[1] = bitop.bxor(state[1], state[4])
state[2] = bitop.bxor(state[3], t)
state[3] = rotl(state[4], 11)
return result
end
local JUMP = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b }
---@param state int[] @array of size 4; will be mutated
local function jump(state)
local s0 = 0
local s1 = 0
local s2 = 0
local s3 = 0
for _, j in ipairs(JUMP) do
for b = 0, 31 do
if bitop.band(j, bitop.lshift(1, b)) ~= 0 then
s0 = bitop.bxor(s0, state[1])
s1 = bitop.bxor(s1, state[2])
s2 = bitop.bxor(s2, state[3])
s3 = bitop.bxor(s3, state[4])
end
next(state)
end
end
state[1] = s0
state[2] = s1
state[3] = s2
state[4] = s3
end
local LONG_JUMP = { 0xb523952e, 0x0b6f099f, 0xccf5a0ef, 0x1c580662 }
---@param state int[] @array of size 4; will be mutated
local function long_jump(state)
local s0 = 0
local s1 = 0
local s2 = 0
local s3 = 0
for _, j in ipairs(LONG_JUMP) do
for b = 0, 31 do
if bitop.band(j, bitop.lshift(1, b)) ~= 0 then
s0 = bitop.bxor(s0, state[1])
s1 = bitop.bxor(s1, state[2])
s2 = bitop.bxor(s2, state[3])
s3 = bitop.bxor(s3, state[4])
end
next(state)
end
end
state[1] = s0
state[2] = s1
state[3] = s2
state[4] = s3
end
---@class rng a xoshiro128** pseudorandom implementation
---@field public state int[] the current state, size of 4
rng = {}
--- gets the next pseudo-random value; recommended to use abstractions (like __call) over this
---@return int
function rng:next()
return next(self.state)
end
--- This is the jump function for the generator. It is equivalent
--- to 2^64 calls to next(); it can be used to generate 2^64
--- non-overlapping subsequences for parallel computations.
---@return void
function rng:jump()
return jump(self.state)
end
--- This is the long-jump function for the generator. It is equivalent to
--- 2^96 calls to next(); it can be used to generate 2^32 starting points,
--- from each of which jump() will generate 2^32 non-overlapping
--- subsequences for parallel distributed computations.
---@return void
function rng:longJump()
return long_jump(self.state)
end
--- if `max` is not given, `min` will be used as the maximum and the minimum will be 1
--- if min is 1 and max is 4, the returned value can be 1, 2, 3 or 4
---@param min int
---@param max int
---@return int
function rng:int(min, max)
if not max then
local m = min
min = 1
max = m
end
local _min = min
local _max = max
min = math.min(_min, _max)
max = math.max(_min, _max)
return min + (self:next() % (max - min + 1))
end
--- if `max` is not given, it will be 1
---@param max float
---@return float
function rng:float(max)
return ((self:next() % RAND_MAX) / RAND_MAX) * (max or 1)
end
---@return boolean
function rng:bool()
return self:next() % 2 == 0
end
function rng:seed(seed)
self.state = {seed, seed, seed, seed}
self:next()
end
local rngmeta = {}
--- acts identical to math.random()
function rngmeta:__call(a, b)
if a then
return self:int(a, b)
end
return self:float()
end
rngmeta.__index = rng
--- creates a new RNG object
---@param seed int[] @array of size 4, will default to os.time() if not given
---@return rng
function rng.init(seed)
seed = seed or os.time()
local state = {seed, seed, seed, seed}
local this = setmetatable({state = state}, rngmeta)
this:next() -- just to prevent the state from being all the same number; i dont know a cleaner way of doing this
return this
end
return rng