Like most languages, JavaScript has a random number generator: Math.random()
. It’s not the best, but it’s good enough for many purposes, including some we’ve seen before. Like the RNGs of most languages, though, Math.random()
has its drawbacks. Of these drawbacks, one is very important for game developers: there’s no way to seed! Why is this bad? That cuts to the very heart of random number generation on computers.
True randomness is hard to come by. On a computer, you have a few ways of creating it, such as noise circuits or measurements of user input. This randomness (entropy) is then converted by code into something usable, such as numbers or bits. On Linux and similar systems, there is a special file, /dev/random
, that provides access to the random data. Some programming languages then allow developers to use the data through mechanisms like C++’s std::random_device
.
All well and good, right? As long as your source has enough randomness, you can generate all the numbers you need. But, contrary to thermodynamics, a computer’s entropy can run out. When that happens, /dev/random
stops working, waiting until it can conjure up enough random bits to meet your needs. Other systems fare no better.
Fortunately for us, games don’t often need true random numbers for gameplay. (Authentication and encryption are a different story.) So we can get away with something that only looks random. That’s good for us, because there are plenty of algorithms out there that can make a convincing string of numbers, including Math.random()
. Technically, they aren’t “true” random numbers, and they all have a weakness that can allow someone with enough data to guess the next numbers in the sequence. But that very predictability comes in handy for game development. Starting from a little bit of data (from one number to a few, depending on the algorithm used), we get a whole set of numbers, billions of them or more. That starting data is the seed.
Most languages that I’ve used allow you to set the seed on the RNG. C, for example, has the srand()
function, while Python provides random.seed()
. But JavaScript doesn’t have this. Instead, the RNG is seeded for you when your program/app/page loads, and it will be different every time. Usually, that’s not a problem.
Sometimes, though, you need the predictable sequence that comes from using a seed. Procedural generation is one notable example. Look at Minecraft: a whole world can be created from a simple string of text. Obviously, there’s randomness injected in the process, but it’s made on-demand. Think how hard it would be to store the whole world after creating it. But, if you only had JavaScript’s RNG, you wouldn’t have much of a choice.
There are better RNG implementations out there. Indeed, many have written JS versions of them. Here’s my attempt at the popular algorithm known as xorshift+
.
module.exports = (function() {
// xorshift128+ requires 128 bits of state (we'll seed later)
var state = new Uint32Array(4);
// Scaling factor (2^32) to convert Math.random floats into integers
var MAXINT_PLUS_1 = Math.pow(2,32)
// Pre-fill the state array (can later be seeded)
// This is required because xorshift can't have state of all zero
for (var i = 0; i < state.length; i++) {
state[i] = Math.random() * MAXINT_PLUS_1;
}
// A saved random number, since we're returning 32-bit numbers
var saved;
return {
// Seeds the internal RNG.
seed: function(s) {
if (s === 0 || s == null) {
// Can't use a zero seed (maybe throw an exception?)
return;
}
// TODO Handle various types of arguments (just numbers/arrays for now)
if (typeof s === 'number') {
// Use only the lowest 32 bits right now
state[0] = s >>> 0;
} else if (s.constructor && s.constructor.name === 'Uint32Array') {
for (var i = 0; i < state.length; i++) {
if (s[i] !== undefined) {
state[i] = s[i];
} else {
state[i] = 0;
}
}
}
},
// Returns a random float between 0 and 1 (exclusive),
// with 32 bits of precision.
random: function() {
// If we already have a saved number, return it,
// also clearing it for later use.
if (saved != null) {
var temp = saved;
saved = null;
return temp / MAXINT_PLUS_1;
}
// x = s[0]
var x = new Uint32Array(2);
x[0] = state[0];
x[1] = state[1];
// y = s[1]
var y = new Uint32Array(2);
y[0] = state[2];
y[1] = state[3];
// s[0] = y
state[0] = y[0];
state[1] = y[1];
// (a): x ^= x << 23
var xl23 = new Uint32Array(2);
xl23[0] = x[0] << 23;
xl23[1] = (x[1] << 23) & (x[0] >> 11);
x[0] ^= xl23[0];
x[1] ^= xl23[1];
// (b): x ^= x >> 17
var xr17 = new Uint32Array(2);
xr17[1] = x[1] >>> 17;
xr17[0] = (x[0] >>> 17) & (x[1] << 19);
x[0] ^= xr17[0];
x[1] ^= xr17[1];
// (c): x ^= y ^ (y >> 26)
var yr26 = new Uint32Array(2);
yr26[1] = y[1] >>> 26;
yr26[0] = (y[0] >>> 26) & (y[1] << 6);
x[0] ^= y[0] ^ yr26[0];
x[1] ^= y[1] ^ yr26[1];
// s[1] = x
state[2] = x[0];
state[3] = x[1];
// return x + y
var retval = new Uint32Array(2);
retval[0] = x[0] + y[0];
retval[1] = x[1] + y[1] + (retval[0] < x[0]);
saved = retval[1];
return retval[0] / MAXINT_PLUS_1;
}
};
})();
I’ve written it as a Node.js module, but you can easily adapt it to a browser environment by changing module.exports
on line 1 to window.xorshift
or whatever you like. Whether it’s attached to the global window
(browser) or loaded with require()
(Node), the function creates an object with two methods: random()
and seed()
, both of which are explained below.
This isn’t a perfect implementation, and it does have a limitation that the original doesn’t have. This is because of JavaScript’s number handling, which might best be termed as “special”. JS only really has 64-bit floats for numbers, unless you do even more TypedArray
contortions than I did here. So I had to compromise by making the random()
function output numbers between 0 and 1 with 32 bits of resolution. Each run of the algorithm creates 64 bits of randomness, so I split that into two numbers, saving the second for the next call to the RNG. Changing the function to return integers instead of floats is easy enough: remove the two divisions by MAXINT_PLUS_1
.
The whole reason for making this thing was to have a predictable RNG for JavaScript. That’s what the seed()
function does. Pass in a single 32-bit integer or a typed array of them, and it will seed the algorithm using that. (One good way to extend this would be to use a hash such as MD5 to allow strings and other objects. That’s why the “TODO” comment is there.) If you don’t, it will use a few numbers generated from Math.random()
.
Another benefit of this (or any similar RNG) over the stock implementation is that you can create more than one, each tied to its own seed. This means that, for example, you can have your world generator running off a different sequence than your AI. You would then only have to save the seed for the world RNG, while the AI RNG gets reseeded when you reload the game. This would prevent players from, say, repeatedly reloading to get a good outcome of a battle in a strategy game.
As usual, I didn’t make the algorithm; I only wrote the code in this post. You can use it for whatever purpose you like, but I’m sure there are better implementations out there. I didn’t check, mostly because I wanted to try my hand at writing it first. It’s good practice.
Until next time, have fun!