pokemon-showdown/prng.js
Dan Pantry 56ba782966 refactor PRNG to its own class
This removes the 'deterministic test' tools by preventing action at a
distance (namely, preventing the modification of the `init` method in
`Battle` during tests). This action at a distance is incredibly
confusing.

All this action at a distance did was discard any parameters that were
passed to `Battle` that weren't the first three (which was probably a
mistake by the original author) and also hard code `this.seed` and
`this.startingSeed` in `Battle`.

This functionality has now been moved to the `PRNG` class, so instead
users should pass a `PRNG` to `Battle` as the 5th constructor argument.
Users can also pass one as the third argument to `common.createBattle`
or use `common.createBattleWithPRNG` with the PRNG as the first
argument.

The PRNG is just an encapsulation of the pseudo-random algorithm in a
class. It is stateful, so make sure to take a `clone()` of the PRNG if
you want to re-use it.
2017-02-22 20:44:20 -05:00

174 lines
5.4 KiB
JavaScript

'use strict';
// This function is designed to emulate the on-cartridge PRNG for Gens 3 and 4, as described in
// http://www.smogon.com/ingame/rng/pid_iv_creation#pokemon_random_number_generator
// This RNG uses a 32-bit initial seed
// m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
/*
random(m, n) {
this.seed = (this.seed * 0x41C64E6D + 0x6073) >>> 0; // truncate the result to the last 32 bits
let result = this.seed >>> 16; // the first 16 bits of the seed are the random value
m = Math.floor(m)
n = Math.floor(n)
return (m ? (n ? (result % (n - m)) + m : result % m) : result / 0x10000)
}
*/
// This function is designed to emulate the on-cartridge PRNG for Gen 5 and uses a 64-bit initial seed
// This function has three different results, depending on arguments:
// - random() returns a real number in [0, 1), just like Math.random()
// - random(n) returns an integer in [0, n)
// - random(m, n) returns an integer in [m, n)
// m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
class PRNG {
/**
* Creates a new source of randomness for the given seed.
*
* @param {number[]} [seed]
*/
constructor(seed = PRNG.generateSeed()) {
// We slice() the seed so we get a copy of it instead of the original seed.
this.initialSeed = seed.slice();
this.seed = seed.slice();
}
/**
* Getter to the initial seed.
*
* This should be considered a hack and is only here for backwards compatibility.
*/
get startingSeed() {
return this.initialSeed;
}
/**
* Creates a clone of the current PRNG.
*
* The new PRNG will have its initial seed set to the seed of the current instance.
*/
clone() {
return new PRNG(this.seed);
}
/**
* Retrieves the next random number in the sequence.
*
* @param {number} [from]
* @param {number} [to]
* @returns {number} a real number in [0, 1) if no arguments are specified
* @returns {number} an integer in [0, from) if from is specified
* @returns {number} an integer in [from, to) if from and to are specified
*/
next(from, to) {
this.seed = this.nextFrame(this.seed); // Advance the RNG
let result = (this.seed[0] << 16 >>> 0) + this.seed[1]; // Use the upper 32 bits
from = Math.floor(from);
to = Math.floor(to);
if (!from) {
result = result / 0x100000000;
} else if (!to) {
result = Math.floor(result * from / 0x100000000);
} else {
result = Math.floor(result * (to - from) / 0x100000000) + from;
}
return result;
}
/**
The RNG is a Linear Congruential Generator (LCG) in the form: `x_{n + 1} = (a x_n + c) % m`
Where: `x_0` is the seed, `x_n` is the random number after n iterations,
````
a = 0x5D588B656C078965
c = 0x00269EC3
m = 2^64
````
Javascript doesnt handle such large numbers properly, so this function does it in 16-bit parts.
````
x_{n + 1} = (x_n * a) + c
````
Let any 64 bit number:
````
n = (n[0] << 48) + (n[1] << 32) + (n[2] << 16) + n[3]
````
Then:
````
x_{n + 1} =
((a[3] x_n[0] + a[2] x_n[1] + a[1] x_n[2] + a[0] x_n[3] + c[0]) << 48) +
((a[3] x_n[1] + a[2] x_n[2] + a[1] x_n[3] + c[1]) << 32) +
((a[3] x_n[2] + a[2] x_n[3] + c[2]) << 16) +
a[3] x_n[3] + c[3]
````
Which can be generalised where b is the number of 16 bit words in the number:
````
((a[b-1] + x_n[b-1] + c[b-1]) << (16 * 0)) +
((a[b-1] x_n[b-2] + a[b-2] x_n[b-1] + c[b-2]) << (16 * 1)) +
((a[b-1] x_n[b-3] + a[b-2] x_n[b-2] + a[b-3] x_n[b-1] + c[b-3]) << (16 * 2)) +
...
((a[b-1] x_n[1] + a[b-2] x_n[2] + ... + a[2] x_n[b-2] + a[1] + x_n[b-1] + c[1]) << (16 * (b-2))) +
((a[b-1] x_n[0] + a[b-2] x_n[1] + ... + a[1] x_n[b-2] + a[0] + x_n[b-1] + c[0]) << (16 * (b-1)))
````
Which produces this equation:
````
\sum_{l=0}^{b-1}\left(\sum_{m=b-l-1}^{b-1}\left\{a[2b-m-l-2] x_n[m]\right\}+c[b-l-1]\ll16l\right)
````
Notice how the `a[]` word starts at `b-1`, and decrements every time it appears again on the line;
`x_n[]` starts at `b-<line#>-1` and increments to b-1 at the end of the line per line, limiting the length of the line;
`c[]` is at `b-<line#>-1` for each line and the left shift is `16 * <line#>`)
This is all ignoring overflow/carry because that cannot be shown in a pseudo-mathematical equation.
The below code implements a optimised version of that equation while also checking for overflow/carry.
@param {number} [framesToAdvance = 1]
@return {number[]} the new seed
*/
nextFrame(initialSeed, framesToAdvance = 1) {
// Use Slice so we don't actually alter the original seed.
let seed = initialSeed.slice();
for (let frame = 0; frame < framesToAdvance; ++frame) {
const a = [0x5D58, 0x8B65, 0x6C07, 0x8965];
const c = [0, 0, 0x26, 0x9EC3];
const nextSeed = [0, 0, 0, 0];
let carry = 0;
for (let cN = seed.length - 1; cN >= 0; --cN) {
nextSeed[cN] = carry;
carry = 0;
let aN = seed.length - 1;
for (let seedN = cN; seedN < seed.length; --aN, ++seedN) {
let nextWord = a[aN] * seed[seedN];
carry += nextWord >>> 16;
nextSeed[cN] += nextWord & 0xFFFF;
}
nextSeed[cN] += c[cN];
carry += nextSeed[cN] >>> 16;
nextSeed[cN] &= 0xFFFF;
}
seed = nextSeed;
}
return seed;
}
static generateSeed() {
// use a random initial seed (64-bit, [high -> low])
return [
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
Math.floor(Math.random() * 0x10000),
];
}
}
module.exports = PRNG;