12
12
* @license MIT license
13
13
*/
14
14
15
+ import { Chacha20 } from 'ts-chacha20' ;
16
+ import { Utils } from '../lib/utils' ;
17
+
18
+ export type PRNGSeed = SodiumRNGSeed | Gen5RNGSeed ;
19
+ export type SodiumRNGSeed = [ 'sodium' , string ] ;
15
20
/** 64-bit big-endian [high -> low] int */
16
- export type PRNGSeed = [ number , number , number , number ] ;
21
+ export type Gen5RNGSeed = [ number , number , number , number ] ;
17
22
18
23
/**
19
- * A PRNG intended to emulate the on-cartridge PRNG for Gen 5 with a 64-bit
20
- * initial seed.
24
+ * Low-level source of 32-bit random numbers.
25
+ */
26
+ interface RNG {
27
+ getSeed ( ) : PRNGSeed ;
28
+ /** random 32-bit number */
29
+ next ( ) : number ;
30
+ }
31
+
32
+ /**
33
+ * High-level PRNG API, for getting random numbers.
34
+ *
35
+ * Chooses the RNG implementation based on the seed passed to the constructor.
36
+ * Seeds starting with 'sodium' use sodium. Other seeds use the Gen 5 RNG.
37
+ * If a seed isn't given, defaults to sodium.
38
+ *
39
+ * The actual randomness source is in this.rng.
21
40
*/
22
41
export class PRNG {
23
- readonly initialSeed : PRNGSeed ;
24
- seed : PRNGSeed ;
42
+ readonly startingSeed : PRNGSeed ;
43
+ rng ! : RNG ;
25
44
/** Creates a new source of randomness for the given seed. */
26
- constructor ( seed : PRNGSeed | null = null ) {
45
+ constructor ( seed : PRNGSeed | null = null , initialSeed ?: PRNGSeed ) {
27
46
if ( ! seed ) seed = PRNG . generateSeed ( ) ;
28
- this . initialSeed = seed . slice ( ) as PRNGSeed ; // make a copy
29
- this . seed = seed . slice ( ) as PRNGSeed ;
47
+ this . startingSeed = initialSeed || [ ... seed ] ; // make a copy
48
+ this . setSeed ( seed ) ;
30
49
}
31
50
32
- /**
33
- * Getter to the initial seed.
34
- *
35
- * This should be considered a hack and is only here for backwards compatibility.
36
- */
37
- get startingSeed ( ) : PRNGSeed {
38
- return this . initialSeed ;
51
+ setSeed ( seed : PRNGSeed ) {
52
+ if ( seed [ 0 ] === 'sodium' ) {
53
+ this . rng = new SodiumRNG ( seed ) ;
54
+ } else {
55
+ this . rng = new Gen5RNG ( seed as Gen5RNGSeed ) ;
56
+ }
57
+ }
58
+ getSeed ( ) : PRNGSeed {
59
+ return this . rng . getSeed ( ) ;
39
60
}
40
61
41
62
/**
@@ -44,7 +65,7 @@ export class PRNG {
44
65
* The new PRNG will have its initial seed set to the seed of the current instance.
45
66
*/
46
67
clone ( ) : PRNG {
47
- return new PRNG ( this . seed ) ;
68
+ return new PRNG ( this . rng . getSeed ( ) , this . startingSeed ) ;
48
69
}
49
70
50
71
/**
@@ -55,19 +76,18 @@ export class PRNG {
55
76
* - random(m, n) returns an integer in [m, n)
56
77
* m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
57
78
*/
58
- next ( from ?: number , to ?: number ) : number {
59
- this . seed = this . nextFrame ( this . seed ) ; // Advance the RNG
60
- let result = ( this . seed [ 0 ] << 16 >>> 0 ) + this . seed [ 1 ] ; // Use the upper 32 bits
79
+ random ( from ?: number , to ?: number ) : number {
80
+ const result = this . rng . next ( ) ;
81
+
61
82
if ( from ) from = Math . floor ( from ) ;
62
83
if ( to ) to = Math . floor ( to ) ;
63
84
if ( from === undefined ) {
64
- result = result / 0x100000000 ;
85
+ return result / 2 ** 32 ;
65
86
} else if ( ! to ) {
66
- result = Math . floor ( result * from / 0x100000000 ) ;
87
+ return Math . floor ( result * from / 2 ** 32 ) ;
67
88
} else {
68
- result = Math . floor ( result * ( to - from ) / 0x100000000 ) + from ;
89
+ return Math . floor ( result * ( to - from ) / 2 ** 32 ) + from ;
69
90
}
70
- return result ;
71
91
}
72
92
73
93
/**
@@ -81,7 +101,7 @@ export class PRNG {
81
101
* The denominator must be a positive integer (`> 0`).
82
102
*/
83
103
randomChance ( numerator : number , denominator : number ) : boolean {
84
- return this . next ( denominator ) < numerator ;
104
+ return this . random ( denominator ) < numerator ;
85
105
}
86
106
87
107
/**
@@ -101,7 +121,7 @@ export class PRNG {
101
121
if ( items . length === 0 ) {
102
122
throw new RangeError ( `Cannot sample an empty array` ) ;
103
123
}
104
- const index = this . next ( items . length ) ;
124
+ const index = this . random ( items . length ) ;
105
125
const item = items [ index ] ;
106
126
if ( item === undefined && ! Object . prototype . hasOwnProperty . call ( items , index ) ) {
107
127
throw new RangeError ( `Cannot sample a sparse array` ) ;
@@ -117,21 +137,94 @@ export class PRNG {
117
137
*/
118
138
shuffle < T > ( items : T [ ] , start = 0 , end : number = items . length ) {
119
139
while ( start < end - 1 ) {
120
- const nextIndex = this . next ( start , end ) ;
140
+ const nextIndex = this . random ( start , end ) ;
121
141
if ( start !== nextIndex ) {
122
142
[ items [ start ] , items [ nextIndex ] ] = [ items [ nextIndex ] , items [ start ] ] ;
123
143
}
124
144
start ++ ;
125
145
}
126
146
}
127
147
148
+ static generateSeed ( ) : SodiumRNGSeed {
149
+ return [
150
+ 'sodium' ,
151
+ // 32 bits each, 128 bits total (16 bytes)
152
+ Math . trunc ( Math . random ( ) * 2 ** 32 ) . toString ( 16 ) . padStart ( 8 , '0' ) +
153
+ Math . trunc ( Math . random ( ) * 2 ** 32 ) . toString ( 16 ) . padStart ( 8 , '0' ) +
154
+ Math . trunc ( Math . random ( ) * 2 ** 32 ) . toString ( 16 ) . padStart ( 8 , '0' ) +
155
+ Math . trunc ( Math . random ( ) * 2 ** 32 ) . toString ( 16 ) . padStart ( 8 , '0' ) ,
156
+ ] ;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * This is a drop-in replacement for libsodium's randombytes_buf_deterministic,
162
+ * but it's implemented with ts-chacha20 instead, for a smaller dependency that
163
+ * doesn't use NodeJS native modules, for better portability.
164
+ */
165
+ export class SodiumRNG implements RNG {
166
+ // nonce chosen to be compatible with libsodium's randombytes_buf_deterministic
167
+ // https://github.com/jedisct1/libsodium/blob/ce07d6c82c0e6c75031cf627913bf4f9d3f1e754/src/libsodium/randombytes/randombytes.c#L178
168
+ static readonly NONCE = Uint8Array . from ( [ ..."LibsodiumDRG" ] . map ( c => c . charCodeAt ( 0 ) ) ) ;
169
+ seed ! : Uint8Array ;
170
+ /** Creates a new source of randomness for the given seed. */
171
+ constructor ( seed : SodiumRNGSeed ) {
172
+ this . setSeed ( seed ) ;
173
+ }
174
+
175
+ setSeed ( seed : SodiumRNGSeed ) {
176
+ // randombytes_buf_deterministic requires 32 bytes, but
177
+ // generateSeed generates 16 bytes, so the last 16 bytes will be 0
178
+ // when starting out. This shouldn't cause any problems.
179
+ const seedBuf = new Uint8Array ( 32 ) ;
180
+ Utils . bufWriteHex ( seedBuf , seed [ 1 ] . padEnd ( 64 , '0' ) ) ;
181
+ this . seed = seedBuf ;
182
+ }
183
+ getSeed ( ) : SodiumRNGSeed {
184
+ return [ 'sodium' , Utils . bufReadHex ( this . seed ) ] ;
185
+ }
186
+
187
+ next ( ) {
188
+ const zeroBuf = new Uint8Array ( 36 ) ;
189
+ // tested to do the exact same thing as
190
+ // sodium.randombytes_buf_deterministic(buf, this.seed);
191
+ const buf = new Chacha20 ( this . seed , SodiumRNG . NONCE ) . encrypt ( zeroBuf ) ;
192
+
193
+ // use the first 32 bytes for the next seed, and the next 4 bytes for the output
194
+ this . seed = buf . slice ( 0 , 32 ) ;
195
+ // reading big-endian
196
+ return buf . slice ( 32 , 36 ) . reduce ( ( a , b ) => a * 256 + b ) ;
197
+ // alternative, probably slower (TODO: benchmark)
198
+ // return parseInt(Utils.bufReadHex(buf, 32, 36), 16);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * A PRNG intended to emulate the on-cartridge PRNG for Gen 5 with a 64-bit
204
+ * initial seed.
205
+ */
206
+ export class Gen5RNG implements RNG {
207
+ seed : Gen5RNGSeed ;
208
+ /** Creates a new source of randomness for the given seed. */
209
+ constructor ( seed : Gen5RNGSeed | null = null ) {
210
+ this . seed = [ ...seed || Gen5RNG . generateSeed ( ) ] ;
211
+ }
212
+
213
+ getSeed ( ) {
214
+ return this . seed ;
215
+ }
216
+
217
+ next ( ) : number {
218
+ this . seed = this . nextFrame ( this . seed ) ; // Advance the RNG
219
+ return ( this . seed [ 0 ] << 16 >>> 0 ) + this . seed [ 1 ] ; // Use the upper 32 bits
220
+ }
221
+
128
222
/**
129
223
* Calculates `a * b + c` (with 64-bit 2's complement integers)
130
- *
131
- * If you've done long multiplication, this is the same thing.
132
224
*/
133
- multiplyAdd ( a : PRNGSeed , b : PRNGSeed , c : PRNGSeed ) {
134
- const out : PRNGSeed = [ 0 , 0 , 0 , 0 ] ;
225
+ multiplyAdd ( a : Gen5RNGSeed , b : Gen5RNGSeed , c : Gen5RNGSeed ) {
226
+ // If you've done long multiplication, this is the same thing.
227
+ const out : Gen5RNGSeed = [ 0 , 0 , 0 , 0 ] ;
135
228
let carry = 0 ;
136
229
137
230
for ( let outIndex = 3 ; outIndex >= 0 ; outIndex -- ) {
@@ -160,39 +253,48 @@ export class PRNG {
160
253
* m = 2^64
161
254
* ````
162
255
*/
163
- nextFrame ( seed : PRNGSeed , framesToAdvance = 1 ) : PRNGSeed {
164
- const a : PRNGSeed = [ 0x5D58 , 0x8B65 , 0x6C07 , 0x8965 ] ;
165
- const c : PRNGSeed = [ 0 , 0 , 0x26 , 0x9EC3 ] ;
256
+ nextFrame ( seed : Gen5RNGSeed , framesToAdvance = 1 ) : Gen5RNGSeed {
257
+ const a : Gen5RNGSeed = [ 0x5D58 , 0x8B65 , 0x6C07 , 0x8965 ] ;
258
+ const c : Gen5RNGSeed = [ 0 , 0 , 0x26 , 0x9EC3 ] ;
166
259
167
260
for ( let i = 0 ; i < framesToAdvance ; i ++ ) {
261
+ // seed = seed * a + c
168
262
seed = this . multiplyAdd ( seed , a , c ) ;
169
263
}
170
264
171
265
return seed ;
172
266
}
173
267
174
- static generateSeed ( ) {
268
+ static generateSeed ( ) : Gen5RNGSeed {
175
269
return [
176
- Math . floor ( Math . random ( ) * 0x10000 ) ,
177
- Math . floor ( Math . random ( ) * 0x10000 ) ,
178
- Math . floor ( Math . random ( ) * 0x10000 ) ,
179
- Math . floor ( Math . random ( ) * 0x10000 ) ,
180
- ] as PRNGSeed ;
270
+ Math . trunc ( Math . random ( ) * 2 ** 16 ) ,
271
+ Math . trunc ( Math . random ( ) * 2 ** 16 ) ,
272
+ Math . trunc ( Math . random ( ) * 2 ** 16 ) ,
273
+ Math . trunc ( Math . random ( ) * 2 ** 16 ) ,
274
+ ] ;
181
275
}
182
276
}
183
277
184
- // The following commented-out function is designed to emulate the on-cartridge
278
+ // The following commented-out class is designed to emulate the on-cartridge
185
279
// PRNG for Gens 3 and 4, as described in
186
280
// https://www.smogon.com/ingame/rng/pid_iv_creation#pokemon_random_number_generator
187
281
// This RNG uses a 32-bit initial seed
188
282
// m and n are converted to integers via Math.floor. If the result is NaN, they
189
283
// are ignored.
190
284
/*
191
- random(m: number, n: number) {
192
- this.seed = (this.seed * 0x41C64E6D + 0x6073) >>> 0; // truncate the result to the last 32 bits
193
- let result = this.seed >>> 16; // the first 16 bits of the seed are the random value
194
- m = Math.floor(m)
195
- n = Math.floor(n)
196
- return (m ? (n ? (result % (n - m)) + m : result % m) : result / 0x10000)
285
+ export type Gen3RNGSeed = ['gen3', number];
286
+ export class Gen3RNG implements RNG {
287
+ seed: number;
288
+ constructor(seed: Gen3RNGSeed | null = null) {
289
+ this.seed = seed ? seed[1] : Math.trunc(Math.random() * 2 ** 32);
290
+ }
291
+ getSeed() {
292
+ return ['gen3', this.seed];
293
+ }
294
+ next(): number {
295
+ this.seed = this.seed * 0x41C64E6D + 0x6073) >>> 0; // truncate the result to the last 32 bits
296
+ const val = this.seed >>> 16; // the first 16 bits of the seed are the random value
297
+ return val << 16 >>> 0; // PRNG#random expects a 32-bit number and will divide accordingly
298
+ }
197
299
}
198
300
*/
0 commit comments