2026-07-05 13:49:00 +01:00
|
|
|
-- The number of draws used by the range/spread tests below. High enough to make
|
|
|
|
|
-- coverage failures (a missed endpoint, a skewed bucket) mean a real regression
|
|
|
|
|
-- rather than noise, low enough to keep the suite fast.
|
|
|
|
|
local kSamples = 10000
|
|
|
|
|
|
|
|
|
|
describe('math.random() contract', function()
|
|
|
|
|
-- math.random is overridden in luautils.cpp to forward into xirand. These tests
|
|
|
|
|
-- lock the surface scripts rely on: interval endpoints, integer-ness, and the
|
|
|
|
|
-- half-open float contract. The 1-in-33-million rounding edge (a float draw
|
|
|
|
|
-- landing exactly on the excluded max) is locked at compile time by the
|
|
|
|
|
-- static_asserts in src/test/tests/rng_spread_tests.cpp; these tests lock the
|
|
|
|
|
-- everyday shape of the API.
|
|
|
|
|
|
|
|
|
|
it('math.random() returns floats in [0, 1)', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.random()
|
|
|
|
|
assert(value >= 0 and value < 1, string.format('math.random() out of [0, 1): %.17g', value))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.random(n) returns integers in [1, n] and reaches both endpoints', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local seen = {}
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.random(6)
|
|
|
|
|
assert(value >= 1 and value <= 6, string.format('math.random(6) out of [1, 6]: %s', tostring(value)))
|
|
|
|
|
assert(value == math.floor(value), 'math.random(6) should return a whole number')
|
|
|
|
|
seen[value] = true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(seen[1], 'math.random(6) never produced its lower endpoint 1')
|
|
|
|
|
assert(seen[6], 'math.random(6) never produced its upper endpoint 6')
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.random(n, m) is inclusive of both endpoints', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local seen = {}
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.random(-3, 3)
|
|
|
|
|
assert(value >= -3 and value <= 3, string.format('math.random(-3, 3) out of range: %s', tostring(value)))
|
|
|
|
|
seen[value] = true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(seen[-3], 'math.random(-3, 3) never produced its lower endpoint -3')
|
|
|
|
|
assert(seen[3], 'math.random(-3, 3) never produced its upper endpoint 3')
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.random(n, n) returns n', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, 100 do
|
|
|
|
|
assert(math.random(4, 4) == 4, 'math.random(4, 4) should always return 4')
|
|
|
|
|
assert(math.random(1) == 1, 'math.random(1) should always return 1')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
-- math.random mimics stock Lua exactly: every argument form yields integers
|
|
|
|
|
-- except the zero-argument call. Fractional bounds are rounded to the nearest
|
|
|
|
|
-- integer -- scripts wanting a float range must say so by name, via
|
|
|
|
|
-- math.randomFloat.
|
|
|
|
|
|
|
|
|
|
it('math.random(a, b) rounds fractional bounds to the nearest integer', function()
|
2026-07-05 13:49:00 +01:00
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
local seen = {}
|
2026-07-05 13:49:00 +01:00
|
|
|
for _ = 1, kSamples do
|
2026-07-05 14:33:55 +01:00
|
|
|
-- 2.4 and 7.6 round to 2 and 8; the result is an integer in [2, 8].
|
2026-07-05 13:49:00 +01:00
|
|
|
local value = math.random(2.4, 7.6)
|
2026-07-05 14:33:55 +01:00
|
|
|
assert(value == math.floor(value), string.format('math.random(2.4, 7.6) returned a fraction: %.17g', value))
|
|
|
|
|
assert(value >= 2 and value <= 8, string.format('math.random(2.4, 7.6) out of [2, 8]: %s', tostring(value)))
|
|
|
|
|
seen[value] = true
|
2026-07-05 13:49:00 +01:00
|
|
|
end
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
assert(seen[2] and seen[8], 'math.random(2.4, 7.6) should reach both rounded endpoints 2 and 8')
|
2026-07-05 13:49:00 +01:00
|
|
|
end)
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
it('math.random(a, b) with a sub-integer span collapses to a constant', function()
|
|
|
|
|
-- Both bounds of math.random(0.7, 1.1) round to 1, so the roll is always 1.
|
|
|
|
|
-- Scripts wanting a fractional roll must use math.randomFloat(0.7, 1.1).
|
|
|
|
|
for _ = 1, 100 do
|
|
|
|
|
assert(math.random(0.7, 1.1) == 1, 'math.random(0.7, 1.1) should always return 1')
|
|
|
|
|
end
|
|
|
|
|
end)
|
2026-07-05 13:49:00 +01:00
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
it('math.random(n) with a fractional argument rounds it', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
2026-07-05 13:49:00 +01:00
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
2026-07-05 14:33:55 +01:00
|
|
|
-- 2.4 rounds to 2; the result is an integer in [1, 2], never a float in [0, 2.4).
|
|
|
|
|
local value = math.random(2.4)
|
|
|
|
|
assert(value == math.floor(value), string.format('math.random(2.4) returned a fraction: %.17g', value))
|
|
|
|
|
assert(value >= 1 and value <= 2, string.format('math.random(2.4) out of [1, 2]: %s', tostring(value)))
|
2026-07-05 13:49:00 +01:00
|
|
|
end
|
|
|
|
|
end)
|
2026-07-05 14:33:55 +01:00
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
describe('math.randomInt() contract', function()
|
|
|
|
|
-- Custom extension bound in luautils.cpp: identical to math.random(lower, upper),
|
|
|
|
|
-- but explicit about its integer semantics at the call site.
|
2026-07-05 13:49:00 +01:00
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
it('returns integers in [lower, upper] and reaches both endpoints', function()
|
2026-07-05 13:49:00 +01:00
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local seen = {}
|
|
|
|
|
for _ = 1, kSamples do
|
2026-07-05 14:33:55 +01:00
|
|
|
local value = math.randomInt(-3, 3)
|
|
|
|
|
assert(value == math.floor(value), string.format('math.randomInt(-3, 3) returned a fraction: %.17g', value))
|
|
|
|
|
assert(value >= -3 and value <= 3, string.format('math.randomInt(-3, 3) out of [-3, 3]: %s', tostring(value)))
|
2026-07-05 13:49:00 +01:00
|
|
|
seen[value] = true
|
|
|
|
|
end
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
assert(seen[-3], 'math.randomInt(-3, 3) never produced its lower endpoint -3')
|
|
|
|
|
assert(seen[3], 'math.randomInt(-3, 3) never produced its upper endpoint 3')
|
2026-07-05 13:49:00 +01:00
|
|
|
end)
|
|
|
|
|
|
2026-07-05 14:33:55 +01:00
|
|
|
it('rounds fractional bounds to the nearest integer', function()
|
2026-07-05 13:49:00 +01:00
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
2026-07-05 14:33:55 +01:00
|
|
|
-- 2.4 and 7.6 round to 2 and 8.
|
|
|
|
|
local value = math.randomInt(2.4, 7.6)
|
|
|
|
|
assert(value == math.floor(value), string.format('math.randomInt(2.4, 7.6) returned a fraction: %.17g', value))
|
|
|
|
|
assert(value >= 2 and value <= 8, string.format('math.randomInt(2.4, 7.6) out of [2, 8]: %s', tostring(value)))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.randomInt(n, n) returns n', function()
|
|
|
|
|
for _ = 1, 100 do
|
|
|
|
|
assert(math.randomInt(4, 4) == 4, 'math.randomInt(4, 4) should always return 4')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
describe('math.randomFloat() contract', function()
|
|
|
|
|
-- Custom extension bound in luautils.cpp: always a double in [lower, upper),
|
|
|
|
|
-- even when the bounds are integral-valued. This is the only way to request a
|
|
|
|
|
-- float range with whole-number bounds -- LuaJIT cannot tell 7.0 from 7, so
|
|
|
|
|
-- math.random(2.0, 7.0) necessarily rolls integers.
|
|
|
|
|
|
|
|
|
|
it('returns doubles in [lower, upper) even with whole-number bounds', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local sawFraction = false
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.randomFloat(2, 7)
|
|
|
|
|
assert(value >= 2 and value < 7, string.format('math.randomFloat(2, 7) out of [2, 7): %.17g', value))
|
|
|
|
|
if value ~= math.floor(value) then
|
|
|
|
|
sawFraction = true
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(sawFraction, 'math.randomFloat(2, 7) should produce fractional values')
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('rolls real float ranges with sub-integer spans', function()
|
|
|
|
|
-- The self-destruct mobskills use math.randomFloat(0.7, 1.1) as a damage
|
|
|
|
|
-- multiplier; with math.random this span would collapse to a constant 1.
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local minSeen = math.huge
|
|
|
|
|
local maxSeen = -math.huge
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.randomFloat(0.7, 1.1)
|
|
|
|
|
assert(value >= 0.7 and value < 1.1, string.format('math.randomFloat(0.7, 1.1) out of [0.7, 1.1): %.17g', value))
|
|
|
|
|
minSeen = math.min(minSeen, value)
|
|
|
|
|
maxSeen = math.max(maxSeen, value)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(maxSeen > minSeen, 'math.randomFloat(0.7, 1.1) should vary, not collapse to a constant')
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.randomFloat(n, n) returns n', function()
|
|
|
|
|
for _ = 1, 100 do
|
|
|
|
|
assert(math.randomFloat(5, 5) == 5, 'math.randomFloat(5, 5) should always return 5')
|
2026-07-05 13:49:00 +01:00
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-07-05 14:08:12 +01:00
|
|
|
describe('math.randomNormal() contract', function()
|
|
|
|
|
-- math.randomNormal is bound in luautils.cpp on top of xirand's inverse-CDF
|
|
|
|
|
-- sampler: one engine draw per call, exact truncation, no rejection loops.
|
|
|
|
|
|
|
|
|
|
it('matches the requested mean and standard deviation', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local sum = 0
|
|
|
|
|
local sumSq = 0
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.randomNormal(3.5, 1.5)
|
|
|
|
|
sum = sum + value
|
|
|
|
|
sumSq = sumSq + value * value
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local mean = sum / kSamples
|
|
|
|
|
local stddev = math.sqrt(sumSq / kSamples - mean * mean)
|
|
|
|
|
|
|
|
|
|
assert(math.abs(mean - 3.5) < 0.05, string.format('Mean drifted: expected ~3.5, got %.5f', mean))
|
|
|
|
|
assert(math.abs(stddev - 1.5) < 0.05, string.format('Stddev drifted: expected ~1.5, got %.5f', stddev))
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('respects both truncation bounds', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local minSeen = math.huge
|
|
|
|
|
local maxSeen = -math.huge
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.randomNormal(3.5, 1.5, 2, 7)
|
|
|
|
|
assert(value >= 2 and value <= 7, string.format('math.randomNormal(3.5, 1.5, 2, 7) out of [2, 7]: %.17g', value))
|
|
|
|
|
minSeen = math.min(minSeen, value)
|
|
|
|
|
maxSeen = math.max(maxSeen, value)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(maxSeen > minSeen, 'Truncated normal should vary, not collapse to a constant')
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('supports one-sided bounds via nil', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local lowerOnly = math.randomNormal(3.5, 1.5, 0)
|
|
|
|
|
assert(lowerOnly >= 0, string.format('math.randomNormal(3.5, 1.5, 0) below lower bound: %.17g', lowerOnly))
|
|
|
|
|
|
|
|
|
|
local upperOnly = math.randomNormal(3.5, 1.5, nil, 4)
|
|
|
|
|
assert(upperOnly <= 4, string.format('math.randomNormal(3.5, 1.5, nil, 4) above upper bound: %.17g', upperOnly))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('handles degenerate inputs', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, 100 do
|
|
|
|
|
-- Zero stddev collapses to mean, clamped into any bounds.
|
|
|
|
|
assert(math.randomNormal(5, 0) == 5, 'math.randomNormal(5, 0) should always return 5')
|
|
|
|
|
assert(math.randomNormal(5, 0, nil, 3) == 3, 'math.randomNormal(5, 0, nil, 3) should clamp to 3')
|
|
|
|
|
assert(math.randomNormal(5, 0, 7) == 7, 'math.randomNormal(5, 0, 7) should clamp to 7')
|
|
|
|
|
|
|
|
|
|
-- Empty interval returns the lower bound, mirroring math.random(n, n < m).
|
|
|
|
|
assert(math.randomNormal(5, 1.5, 7, 2) == 7, 'math.randomNormal with inverted bounds should return lower')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('reproduces the same stream for the same seed', function()
|
|
|
|
|
local first = {}
|
|
|
|
|
xi.test.world:setSeed(42)
|
|
|
|
|
for _ = 1, 8 do
|
|
|
|
|
table.insert(first, math.randomNormal(3.5, 1.5, 2, 7))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local second = {}
|
|
|
|
|
xi.test.world:setSeed(42)
|
|
|
|
|
for _ = 1, 8 do
|
|
|
|
|
table.insert(second, math.randomNormal(3.5, 1.5, 2, 7))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, 8 do
|
|
|
|
|
assert(first[i] == second[i], string.format('Element %d: %.17g ~= %.17g', i, first[i], second[i]))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
2026-07-05 13:49:00 +01:00
|
|
|
describe('PRNG distribution', function()
|
|
|
|
|
it('math.random() spreads evenly across buckets', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local buckets = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
|
|
|
|
|
local total = 0
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.random()
|
|
|
|
|
buckets[math.floor(value * 10) + 1] = buckets[math.floor(value * 10) + 1] + 1
|
|
|
|
|
total = total + value
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local ideal = kSamples / 10
|
|
|
|
|
for i = 1, 10 do
|
|
|
|
|
assert(
|
|
|
|
|
math.abs(buckets[i] - ideal) < ideal * 0.2,
|
|
|
|
|
string.format('Bucket %d skewed: expected ~%d, got %d', i, ideal, buckets[i])
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local mean = total / kSamples
|
|
|
|
|
assert(math.abs(mean - 0.5) < 0.02, string.format('Mean drifted: expected ~0.5, got %.5f', mean))
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('math.random(n) covers every value roughly evenly', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local counts = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local value = math.random(10)
|
|
|
|
|
counts[value] = counts[value] + 1
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local ideal = kSamples / 10
|
|
|
|
|
for i = 1, 10 do
|
|
|
|
|
assert(
|
|
|
|
|
math.abs(counts[i] - ideal) < ideal * 0.2,
|
|
|
|
|
string.format('Value %d skewed: expected ~%d, got %d', i, ideal, counts[i])
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
describe('PRNG usage patterns', function()
|
|
|
|
|
-- Idioms used across scripts/, locked here so an engine or binding change that
|
|
|
|
|
-- breaks them fails loudly instead of corrupting gameplay quietly.
|
|
|
|
|
|
|
|
|
|
it('1 - math.random() is always a valid input to math.log()', function()
|
|
|
|
|
-- Sampling transforms (e.g. Box-Muller for normal distributions) rely on
|
|
|
|
|
-- math.random() < 1 so that math.log(1 - math.random()) is finite.
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local u = 1 - math.random()
|
|
|
|
|
assert(u > 0, 'math.random() returned 1.0; log(0) would be -inf')
|
|
|
|
|
|
|
|
|
|
local logValue = math.log(u)
|
|
|
|
|
assert(logValue <= 0 and logValue == logValue, 'math.log(1 - math.random()) must be finite')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('random angles stay strictly below 2 * pi', function()
|
|
|
|
|
-- Pattern from npc_util.lua and mob repositioning scripts.
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
local angle = math.random() * 2 * math.pi
|
|
|
|
|
assert(angle >= 0 and angle < 2 * math.pi, string.format('Angle out of [0, 2*pi): %.17g', angle))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('random table indexing never overruns', function()
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local t = { 'a', 'b', 'c', 'd', 'e', 'f', 'g' }
|
|
|
|
|
for _ = 1, kSamples do
|
|
|
|
|
assert(t[math.random(#t)] ~= nil, 'math.random(#t) produced an invalid index')
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end)
|
|
|
|
|
|
2025-08-21 23:54:26 -06:00
|
|
|
describe('PRNG', function()
|
|
|
|
|
it('can be forced to a specific seed', function()
|
2026-07-04 12:49:08 +01:00
|
|
|
local expected = { 141, 597, 964, 998, 667, 697, 572, 741, 488, 371 }
|
2025-08-21 23:54:26 -06:00
|
|
|
local actual = {}
|
|
|
|
|
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
for _ = 1, 10 do
|
|
|
|
|
table.insert(actual, math.random(1000))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, #expected do
|
|
|
|
|
assert(expected[i] == actual[i], string.format('Element %d: expected %d, got %d', i, expected[i], actual[i]))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2026-07-05 13:49:00 +01:00
|
|
|
it('reproduces the same float stream for the same seed', function()
|
|
|
|
|
local first = {}
|
|
|
|
|
xi.test.world:setSeed(42)
|
|
|
|
|
for _ = 1, 8 do
|
|
|
|
|
table.insert(first, math.random())
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local second = {}
|
|
|
|
|
xi.test.world:setSeed(42)
|
|
|
|
|
for _ = 1, 8 do
|
|
|
|
|
table.insert(second, math.random())
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, 8 do
|
|
|
|
|
assert(first[i] == second[i], string.format('Element %d: %.17g ~= %.17g', i, first[i], second[i]))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
it('produces a stable golden float stream', function()
|
|
|
|
|
-- Bit-exact goldens for the default engine (Squirrel5 + canonical53 + the
|
|
|
|
|
-- float narrowing in the math.random binding). The RNG stack is deterministic
|
|
|
|
|
-- on all platforms, so any drift here is a real behavior change, not noise.
|
|
|
|
|
local expected =
|
|
|
|
|
{
|
|
|
|
|
0.1404843523841196,
|
|
|
|
|
0.96306491641577729,
|
|
|
|
|
0.66603129130620931,
|
|
|
|
|
0.57130454287094867,
|
|
|
|
|
0.48767078198955727,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xi.test.world:setSeed(1)
|
|
|
|
|
|
|
|
|
|
local actuals = {}
|
|
|
|
|
for i = 1, #expected do
|
|
|
|
|
actuals[i] = math.random()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for i = 1, #expected do
|
|
|
|
|
assert(expected[i] == actuals[i], string.format('Element %d: expected %.17g, got %.17g', i, expected[i], actuals[i]))
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2025-08-21 23:54:26 -06:00
|
|
|
it('test seed does not leak', function()
|
2026-07-04 12:49:08 +01:00
|
|
|
local expected = { 141, 597, 964, 998, 667, 697, 572, 741, 488, 371 }
|
2025-08-21 23:54:26 -06:00
|
|
|
local actual = {}
|
|
|
|
|
|
|
|
|
|
for _ = 1, 10 do
|
|
|
|
|
table.insert(actual, math.random(1000))
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Check that at least one element is different
|
|
|
|
|
local allSame = true
|
|
|
|
|
for i = 1, #expected do
|
|
|
|
|
if expected[i] ~= actual[i] then
|
|
|
|
|
allSame = false
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert(not allSame, 'Arrays should not be the same')
|
|
|
|
|
end)
|
|
|
|
|
end)
|