├── .gitignore ├── README.md ├── UNLICENSE ├── bower.json ├── index.js ├── package.json ├── rng.js └── test ├── browserify.js └── cjs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript seedable random number generator 2 | 3 | The generator below, seeded with "`Example`" will produce the same 4 | values as below for each of these random number distributions across 5 | all browsers. 6 | 7 | ```javascript 8 | var rng = new RNG('Example'); 9 | rng.random(40, 50); // => 42 10 | rng.uniform(); // => 0.7972798995050903 11 | rng.normal(); // => -0.6698504543216376 12 | rng.exponential(); // => 1.0547367609131555 13 | rng.poisson(4); // => 2 14 | rng.gamma(4); // => 2.781724687386858 15 | ``` 16 | 17 | The underlying algorithm is RC4 and uniform number generation is about 18 | 10x slower than `Math.random` in V8. What you get in exchange for that 19 | is a seedable generator and additional random distributions (see 20 | example). You can still get speed *and* these additional distributions 21 | by using Math.random as the core uniform number generator. 22 | 23 | ```javascript 24 | var rng = new RNG(Math.random); 25 | ``` 26 | 27 | When no seed is provided, one is created randomly from available 28 | entropy sources. Seeds that are not strings are run through 29 | JSON.stringify() before being used. 30 | 31 | Here's how you would replace `Math.random` with a seeded generator. 32 | 33 | ```javascript 34 | Math.random = RNG.prototype.uniform.bind(new RNG('my seed')); 35 | ``` 36 | 37 | Finally, for fun, a dice roller, 38 | 39 | ```javascript 40 | var dice = RNG.roller('4d6 + 10'); 41 | dice(); // => 17 42 | dice(); // => 11 43 | ``` 44 | 45 | ## Node.js Usage 46 | 47 | ```javascript 48 | var RNG = require('rng-js'); 49 | ``` 50 | 51 | This module can also be [browserified][browserify] thanks to 52 | [browserify-shim][shim]. 53 | 54 | 55 | [browserify]: https://github.com/substack/node-browserify 56 | [shim]: https://github.com/thlorenz/browserify-shim 57 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rng-js", 3 | "version": "1.0.1", 4 | "main": "rng.js", 5 | "description": "Seedable random number generator functions.", 6 | "license": "Public Domain" 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var vm = require('vm'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | // You shouldn't need to specify the whole path, but tap doesn't like a 6 | // relative path when testing. 7 | var localPath = path.join(__dirname, 'rng.js'); 8 | var src = fs.readFileSync(localPath, { encoding: 'utf8' }); 9 | 10 | var ctx = { window: {} }; 11 | vm.runInNewContext(src, ctx); 12 | 13 | module.exports = ctx.RNG; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rng-js", 3 | "version": "1.0.1", 4 | "description": "Seedable random number generator functions.", 5 | "browser": "rng.js", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "tap test/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/skeeto/rng-js.git" 13 | }, 14 | "keywords": [ 15 | "random", 16 | "seed", 17 | "poisson", 18 | "gamma", 19 | "rc4", 20 | "exponential" 21 | ], 22 | "author": "Christopher Wellons ", 23 | "license": "Unlicence", 24 | "bugs": { 25 | "url": "https://github.com/skeeto/rng-js/issues" 26 | }, 27 | "homepage": "https://github.com/skeeto/rng-js", 28 | "devDependencies": { 29 | "browserify": "~3.20.0", 30 | "browserify-shim": "~3.1.5", 31 | "tap": "~0.4.8" 32 | }, 33 | "browserify": { 34 | "transform": [ 35 | "browserify-shim" 36 | ] 37 | }, 38 | "browserify-shim": { 39 | "./rng.js": "RNG" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Seedable random number generator functions. 3 | * @version 1.0.0 4 | * @license Public Domain 5 | * 6 | * @example 7 | * var rng = new RNG('Example'); 8 | * rng.random(40, 50); // => 42 9 | * rng.uniform(); // => 0.7972798995050903 10 | * rng.normal(); // => -0.6698504543216376 11 | * rng.exponential(); // => 1.0547367609131555 12 | * rng.poisson(4); // => 2 13 | * rng.gamma(4); // => 2.781724687386858 14 | */ 15 | 16 | /** 17 | * @param {String} seed A string to seed the generator. 18 | * @constructor 19 | */ 20 | function RC4(seed) { 21 | this.s = new Array(256); 22 | this.i = 0; 23 | this.j = 0; 24 | for (var i = 0; i < 256; i++) { 25 | this.s[i] = i; 26 | } 27 | if (seed) { 28 | this.mix(seed); 29 | } 30 | } 31 | 32 | /** 33 | * Get the underlying bytes of a string. 34 | * @param {string} string 35 | * @returns {Array} An array of bytes 36 | */ 37 | RC4.getStringBytes = function(string) { 38 | var output = []; 39 | for (var i = 0; i < string.length; i++) { 40 | var c = string.charCodeAt(i); 41 | var bytes = []; 42 | do { 43 | bytes.push(c & 0xFF); 44 | c = c >> 8; 45 | } while (c > 0); 46 | output = output.concat(bytes.reverse()); 47 | } 48 | return output; 49 | }; 50 | 51 | RC4.prototype._swap = function(i, j) { 52 | var tmp = this.s[i]; 53 | this.s[i] = this.s[j]; 54 | this.s[j] = tmp; 55 | }; 56 | 57 | /** 58 | * Mix additional entropy into this generator. 59 | * @param {String} seed 60 | */ 61 | RC4.prototype.mix = function(seed) { 62 | var input = RC4.getStringBytes(seed); 63 | var j = 0; 64 | for (var i = 0; i < this.s.length; i++) { 65 | j += this.s[i] + input[i % input.length]; 66 | j %= 256; 67 | this._swap(i, j); 68 | } 69 | }; 70 | 71 | /** 72 | * @returns {number} The next byte of output from the generator. 73 | */ 74 | RC4.prototype.next = function() { 75 | this.i = (this.i + 1) % 256; 76 | this.j = (this.j + this.s[this.i]) % 256; 77 | this._swap(this.i, this.j); 78 | return this.s[(this.s[this.i] + this.s[this.j]) % 256]; 79 | }; 80 | 81 | /** 82 | * Create a new random number generator with optional seed. If the 83 | * provided seed is a function (i.e. Math.random) it will be used as 84 | * the uniform number generator. 85 | * @param seed An arbitrary object used to seed the generator. 86 | * @constructor 87 | */ 88 | function RNG(seed) { 89 | if (seed == null) { 90 | seed = '' + Math.random() + Date.now(); 91 | } else if (typeof seed === "function") { 92 | // Use it as a uniform number generator 93 | this.uniform = seed; 94 | this.nextByte = function() { 95 | return ~~(this.uniform() * 256); 96 | }; 97 | seed = null; 98 | } else if (Object.prototype.toString.call(seed) !== "[object String]") { 99 | seed = JSON.stringify(seed); 100 | } 101 | this._normal = null; 102 | if (seed) { 103 | this._state = new RC4(seed); 104 | } else { 105 | this._state = null; 106 | } 107 | } 108 | 109 | /** 110 | * @returns {number} Uniform random number between 0 and 255. 111 | */ 112 | RNG.prototype.nextByte = function() { 113 | return this._state.next(); 114 | }; 115 | 116 | /** 117 | * @returns {number} Uniform random number between 0 and 1. 118 | */ 119 | RNG.prototype.uniform = function() { 120 | var BYTES = 7; // 56 bits to make a 53-bit double 121 | var output = 0; 122 | for (var i = 0; i < BYTES; i++) { 123 | output *= 256; 124 | output += this.nextByte(); 125 | } 126 | return output / (Math.pow(2, BYTES * 8) - 1); 127 | }; 128 | 129 | /** 130 | * Produce a random integer within [n, m). 131 | * @param {number} [n=0] 132 | * @param {number} m 133 | * 134 | */ 135 | RNG.prototype.random = function(n, m) { 136 | if (n == null) { 137 | return this.uniform(); 138 | } else if (m == null) { 139 | m = n; 140 | n = 0; 141 | } 142 | return n + Math.floor(this.uniform() * (m - n)); 143 | }; 144 | 145 | /** 146 | * Generates numbers using this.uniform() with the Box-Muller transform. 147 | * @returns {number} Normally-distributed random number of mean 0, variance 1. 148 | */ 149 | RNG.prototype.normal = function() { 150 | if (this._normal !== null) { 151 | var n = this._normal; 152 | this._normal = null; 153 | return n; 154 | } else { 155 | var x = this.uniform() || Math.pow(2, -53); // can't be exactly 0 156 | var y = this.uniform(); 157 | this._normal = Math.sqrt(-2 * Math.log(x)) * Math.sin(2 * Math.PI * y); 158 | return Math.sqrt(-2 * Math.log(x)) * Math.cos(2 * Math.PI * y); 159 | } 160 | }; 161 | 162 | /** 163 | * Generates numbers using this.uniform(). 164 | * @returns {number} Number from the exponential distribution, lambda = 1. 165 | */ 166 | RNG.prototype.exponential = function() { 167 | return -Math.log(this.uniform() || Math.pow(2, -53)); 168 | }; 169 | 170 | /** 171 | * Generates numbers using this.uniform() and Knuth's method. 172 | * @param {number} [mean=1] 173 | * @returns {number} Number from the Poisson distribution. 174 | */ 175 | RNG.prototype.poisson = function(mean) { 176 | var L = Math.exp(-(mean || 1)); 177 | var k = 0, p = 1; 178 | do { 179 | k++; 180 | p *= this.uniform(); 181 | } while (p > L); 182 | return k - 1; 183 | }; 184 | 185 | /** 186 | * Generates numbers using this.uniform(), this.normal(), 187 | * this.exponential(), and the Marsaglia-Tsang method. 188 | * @param {number} a 189 | * @returns {number} Number from the gamma distribution. 190 | */ 191 | RNG.prototype.gamma = function(a) { 192 | var d = (a < 1 ? 1 + a : a) - 1 / 3; 193 | var c = 1 / Math.sqrt(9 * d); 194 | do { 195 | do { 196 | var x = this.normal(); 197 | var v = Math.pow(c * x + 1, 3); 198 | } while (v <= 0); 199 | var u = this.uniform(); 200 | var x2 = Math.pow(x, 2); 201 | } while (u >= 1 - 0.0331 * x2 * x2 && 202 | Math.log(u) >= 0.5 * x2 + d * (1 - v + Math.log(v))); 203 | if (a < 1) { 204 | return d * v * Math.exp(this.exponential() / -a); 205 | } else { 206 | return d * v; 207 | } 208 | }; 209 | 210 | /** 211 | * Accepts a dice rolling notation string and returns a generator 212 | * function for that distribution. The parser is quite flexible. 213 | * @param {string} expr A dice-rolling, expression i.e. '2d6+10'. 214 | * @param {RNG} rng An optional RNG object. 215 | * @returns {Function} 216 | */ 217 | RNG.roller = function(expr, rng) { 218 | var parts = expr.split(/(\d+)?d(\d+)([+-]\d+)?/).slice(1); 219 | var dice = parseFloat(parts[0]) || 1; 220 | var sides = parseFloat(parts[1]); 221 | var mod = parseFloat(parts[2]) || 0; 222 | rng = rng || new RNG(); 223 | return function() { 224 | var total = dice + mod; 225 | for (var i = 0; i < dice; i++) { 226 | total += rng.random(sides); 227 | } 228 | return total; 229 | }; 230 | }; 231 | 232 | /* Provide a pre-made generator instance. */ 233 | RNG.$ = new RNG(); 234 | -------------------------------------------------------------------------------- /test/browserify.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'), 2 | test = require('tap').test, 3 | vm = require('vm'); 4 | 5 | // This code is taken nearly verbatim from browserify-shim's tests: 6 | // https://github.com/thlorenz/browserify-shim/blob/master/test/bundle-deps.js 7 | 8 | test('Bundle via browserify works', function(t) { 9 | 10 | var relPath = '../'; 11 | 12 | browserify( { ignoreGlobals: true }) 13 | .require(require.resolve(relPath)) 14 | .bundle(function (err, src) { 15 | 16 | if (err) { t.fail(err); return t.end(); } 17 | 18 | var ctx = { window: {}, console: console }; 19 | ctx.self = ctx.window; 20 | var require_ = vm.runInNewContext(src, ctx); 21 | 22 | var RNG = require_(require.resolve(relPath)); 23 | 24 | t.ok(RNG, 'RNG is defined'); 25 | t.ok(RNG.$, 'Prebuilt instance comes along'); 26 | t.ok(RNG.roller, 'Roller function is present'); 27 | t.end(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/cjs.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | 3 | test('Loading via CJS (node) works', function(t) { 4 | 5 | var RNG = require('../'); 6 | 7 | t.ok(RNG, 'RNG is defined'); 8 | t.ok(RNG.$, 'Prebuilt instance comes along'); 9 | t.ok(RNG.roller, 'Roller function is present'); 10 | t.end(); 11 | }); 12 | --------------------------------------------------------------------------------