Random

Random #

Luanti provides four different random sources, each with its own merits. Modders must choose wisely unless they can let the engine do the random for them (e.g. randomly picking a sound or a texture for particles).

Lua builtins #

Not restricted by mod security, these functions are available to both SSMs and CSMs:

math.randomseed #

Seed the random. Luanti already does this for you using the system time.

Info

Do not seed the random to turn it into a deterministic random source as other mods may expect it to be “non-deterministic”.

Conversely, do not rely on the random to have any particular seed either; other mods & the engine may have seeded it (using the system time) to be “non-deterministic”.

The problem with math.randomseed is that there is only one global, hidden seed. There is no way to get the current seed out; mods can’t restore their random sequence. Mods seeding the random thus necessarily conflict - unless they all expect it to be “non-deterministic” and only seed it accordingly (ideally not at all, since the engine-side seeding should suffice).

If you need math.random for its performance but want it to be deterministic, you may reseed the random after you’re done with it to ensure that it is “non-deterministic” again.

-- Use the random to generate a seed for the random; preferable over using system time,
-- as the latter may be deterministic
local seed = ... -- some fixed seed
local reseed = math.random(2^31-1)
math.randomseed(seed) -- temporarily make the random "deterministic"
-- ... do something using `math.random` ...
math.randomseed(reseed)

math.random #

Get a random number. Very versatile; allows getting floats between 0 and 1 or integers in a range.

Note

The random numbers between 0 and 1 do not provide a full 52-bit mantissa full of entropy; they usually have around 32 bits of entropy.

Warning

When using this to obtain integers, make sure that both the upper & lower bound as well as their difference are within the C int range - otherwise you may get overflows & errors.

Warning

This is not portable; different builds on different platforms will produce different random numbers. PUC Lua 5.1 builds use a system-provided random generator. LuaJIT builds use LuaJIT’s PRNG implementation. Do not use math.random in mapgen, for example.

Tip

Use math.random as your go-to versatile “non-deterministic” random source.

Random Number Generators #

PcgRandom #

A seedable 32-bit signed integer pseudo-random number generator.

PcgRandom(seed) #

Constructs a PcgRandom instance with the given seed, which should be an integer within 32-bit bounds.

:next([min, max]) #

If min and max are both omitted, they default to -2^31 (-2147483648) and 2^31 - 1 (2147483647) respectively.

:rand_normal_dist(min, max, [num_trials]) #

Warning

No successful use of this function is documented. Consider implementing your own normal distribution instead.

min and max are required; they need to be integers.

Rough approximation of a normal distribution with a mean of (max - min) / 2 and a variance of (((max - min + 1) ^ 2) - 1) / (12 * num_trials).

num_trials defaults to 6. The more trials, the better the approximation.

The return value is a float.

PseudoRandom #

A seedable 16-bit unsigned integer pseudo-random number generator.

“Uses a well-known LCG algorithm introduced by K&R.”

Perhaps the lowest-quality random generator of all.

PseudoRandom(seed) #

Constructor: Takes a seed and returns a PseudoRandom object.

:next([min, max]) #

If min and max are both omitted, they default to 0 and 2^16-1 (32767) respectively.

Warning

Requires ((max - min) == 32767) or ((max-min) <= 6553)) for a proper distribution.

SecureRandom #

System-provided cryptographically secure random: An attacker should not be able to predict the generated sequence of random numbers. Use this when generating cryptographic keys or tokens.

Note

On Windows, the Win32 Crypto API is used to retrieve cryptographically secure random values which is available on every supported version of Windows. On any other platform it is retrieved from /dev/urandom which should be available on all Unix-like platforms such as Linux and Android.

SecureRandom() #

Constructor: Returns a SecureRandom object.

Info

Previously this could return nil if it can’t retrieve a source of randomness, but Luanti 5.10 will always return an object and throw an error on very obscure platforms where it is not available. From a modder’s point of view you can rely on it always being available now.

:next_bytes([count]) #

Only argument is count, an optional integer defaulting to 1 and limited to 2048 specifying how many bytes are to be returned. Returned as a Lua bytestring of length count

Benchmarking #

collectgarbage"stop" -- we don't want GC heuristics to interfere

local n = 1e8 -- number of runs
local function bench(name, constructor, invokation)
	local func = assert(loadstring(([[
local r = %s
for _ = 1, %d do %s end
]]):format(constructor, n, invokation)))
	local t = core.get_us_time()
	func()
	print(name, (core.get_us_time() - t) / n, "µs/call")
end

bench("Lua", "nil", "math.random()")
bench("PCG", "PcgRandom(42)", "r:next()")
bench("K&R", "PseudoRandom(42)", "r:next()")
bench("Secure", "assert(SecureRandom())", "r:next_bytes()")

Example output:

Lua	0.00385002	µs/call
PCG	0.05579729	µs/call
K&R	0.05859349	µs/call
Secure	0.11211887	µs/call

Comparison #

Random SourcePerformanceBytes of entropySeedabilityVersatilityDistributionSecurityPortability
math.randomvery good (1x)up to 4global seed; seeded by defaultvery goodno guarantees, but usually decent enoughnot cryptographically securevaries by platform
PcgRandomokay (~14x)up to 4per-instance seedvery goodgood, decent guaranteesnot cryptographically securealways the same
PseudoRandomokay (~15x)1 to 2per-instance seedoutright sucksokay-ishnot cryptographically securealways the same
SecureRandomstill okay (30x)1 to 2048not seedablecryptographically securevaries by platform

Note: The performance comparison is a bit of an apples-to-oranges comparison for multiple reasons:

  1. The different generators make different guarantees regarding the randomness;
  2. The different generators generate different numbers of bytes per invocation - the default was arbitrarily chosen; Secure random in particular is able to generate plenty of bytes (up to 2048) with one call.

The benchmark still suffices to draw basic conclusions though, especially for the common case where a random source is simply used once (e.g. math.random() < 0.5).

Conclusion #

  1. Never use PseudoRandom. It is strictly inferior to PcgRandom.
  2. Use math.random if you want a fast “non-deterministic” random.
  3. Use PcgRandom if you need per-instance seedability and can take the performance hit.
  4. Use SecureRandom if and only if you need a cryptographically secure random.