Skip to content

Commit fde2b11

Browse files
authoredJan 15, 2025··
PRNGSeed is now a string (#10826)
This makes it so we no longer need to ad-hoc convert seeds from strings to arrays when we get them from text protocols like the command line or BattleStream's `reseed` command. It also has the side benefit of making inputlogs very slightly smaller.
1 parent ec7332b commit fde2b11

15 files changed

+62
-46
lines changed
 

‎data/cg-teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export default class TeamGenerator {
111111
this.dex = Dex.forFormat(format);
112112
this.format = Dex.formats.get(format);
113113
this.teamSize = this.format.ruleTable?.maxTeamSize || 6;
114-
this.prng = seed instanceof PRNG ? seed : new PRNG(seed);
114+
this.prng = PRNG.get(seed);
115115
this.itemPool = this.dex.items.all().filter(i => i.exists && i.isNonstandard !== 'Past' && !i.isPokeball);
116116
this.specialItems = {};
117117
for (const i of this.itemPool) {

‎data/random-battles/gen8/teams.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export class RandomGen8Teams {
138138

139139
this.factoryTier = '';
140140
this.format = format;
141-
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
141+
this.prng = PRNG.get(prng);
142142

143143
this.moveEnforcementCheckers = {
144144
screens: (movePool, moves, abilities, types, counter, species, teamDetails) => {
@@ -243,7 +243,7 @@ export class RandomGen8Teams {
243243
}
244244

245245
setSeed(prng?: PRNG | PRNGSeed) {
246-
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
246+
this.prng = PRNG.get(prng);
247247
}
248248

249249
getTeam(options?: PlayerOptions | null): PokemonSet[] {

‎data/random-battles/gen9/teams.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export class RandomTeams {
195195

196196
this.factoryTier = '';
197197
this.format = format;
198-
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
198+
this.prng = PRNG.get(prng);
199199

200200
this.moveEnforcementCheckers = {
201201
Bug: (movePool, moves, abilities, types, counter) => (
@@ -252,7 +252,7 @@ export class RandomTeams {
252252
}
253253

254254
setSeed(prng?: PRNG | PRNGSeed) {
255-
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
255+
this.prng = PRNG.get(prng);
256256
}
257257

258258
getTeam(options?: PlayerOptions | null): PokemonSet[] {

‎pokemon-showdown

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) {
105105
{
106106
ensureBuilt();
107107
var Teams = require('./dist/sim/teams.js').Teams;
108-
var seed = process.argv[4] ? process.argv[4].split(',').map(Number) : undefined;
108+
var seed = process.argv[4] || undefined;
109109
console.log(Teams.pack(Teams.generate(process.argv[3], {seed})));
110110
}
111111
break;

‎sim/battle-stream.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,9 @@ export class BattleStream extends Streams.ObjectReadWriteStream<string> {
136136
this.battle!.inputLog.push(`>forcelose ${message}`);
137137
break;
138138
case 'reseed':
139-
const seed = message ? message.split(',').map(
140-
n => /[0-9]/.test(n.charAt(0)) ? Number(n) : n
141-
) as PRNGSeed : null;
142-
this.battle!.resetRNG(seed);
139+
this.battle!.resetRNG(message as PRNGSeed);
143140
// could go inside resetRNG, but this makes using it in `eval` slightly less buggy
144-
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed().join(',')}`);
141+
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed()}`);
145142
break;
146143
case 'tiebreak':
147144
this.battle!.tiebreak();

‎sim/prng.ts

+40-18
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import {Chacha20} from 'ts-chacha20';
1616
import {Utils} from '../lib/utils';
1717

18-
export type PRNGSeed = SodiumRNGSeed | Gen5RNGSeed;
18+
export type PRNGSeed = `${'sodium' | 'gen5' | number},${string}`;
1919
export type SodiumRNGSeed = ['sodium', string];
2020
/** 64-bit big-endian [high -> low] int */
2121
export type Gen5RNGSeed = [number, number, number, number];
@@ -44,15 +44,27 @@ export class PRNG {
4444
/** Creates a new source of randomness for the given seed. */
4545
constructor(seed: PRNGSeed | null = null, initialSeed?: PRNGSeed) {
4646
if (!seed) seed = PRNG.generateSeed();
47-
this.startingSeed = initialSeed || [...seed]; // make a copy
47+
if (Array.isArray(seed)) {
48+
// compat for old inputlogs
49+
seed = seed.join(',') as PRNGSeed;
50+
}
51+
if (typeof seed !== 'string') {
52+
throw new Error(`PRNG: Seed ${seed} must be a string`);
53+
}
54+
this.startingSeed = initialSeed ?? seed;
4855
this.setSeed(seed);
4956
}
5057

5158
setSeed(seed: PRNGSeed) {
52-
if (seed[0] === 'sodium') {
53-
this.rng = new SodiumRNG(seed);
59+
if (seed.startsWith('sodium,')) {
60+
this.rng = new SodiumRNG(seed.split(',') as SodiumRNGSeed);
61+
} else if (seed.startsWith('gen5,')) {
62+
const gen5Seed = [seed.slice(5, 9), seed.slice(9, 13), seed.slice(13, 17), seed.slice(17, 21)];
63+
this.rng = new Gen5RNG(gen5Seed.map(n => parseInt(n, 16)) as Gen5RNGSeed);
64+
} else if (/[0-9]/.test(seed.charAt(0))) {
65+
this.rng = new Gen5RNG(seed.split(',').map(Number) as Gen5RNGSeed);
5466
} else {
55-
this.rng = new Gen5RNG(seed as Gen5RNGSeed);
67+
throw new Error(`Unrecognized RNG seed ${seed}`);
5668
}
5769
}
5870
getSeed(): PRNGSeed {
@@ -145,15 +157,14 @@ export class PRNG {
145157
}
146158
}
147159

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-
];
160+
static generateSeed(): PRNGSeed {
161+
return PRNG.convertSeed(SodiumRNG.generateSeed());
162+
}
163+
static convertSeed(seed: SodiumRNGSeed | Gen5RNGSeed): PRNGSeed {
164+
return seed.join(',') as PRNGSeed;
165+
}
166+
static get(prng?: PRNG | PRNGSeed | null) {
167+
return prng && typeof prng !== 'string' && !Array.isArray(prng) ? prng : new PRNG(prng as PRNGSeed);
157168
}
158169
}
159170

@@ -180,8 +191,8 @@ export class SodiumRNG implements RNG {
180191
Utils.bufWriteHex(seedBuf, seed[1].padEnd(64, '0'));
181192
this.seed = seedBuf;
182193
}
183-
getSeed(): SodiumRNGSeed {
184-
return ['sodium', Utils.bufReadHex(this.seed)];
194+
getSeed(): PRNGSeed {
195+
return `sodium,${Utils.bufReadHex(this.seed)}`;
185196
}
186197

187198
next() {
@@ -197,6 +208,17 @@ export class SodiumRNG implements RNG {
197208
// alternative, probably slower (TODO: benchmark)
198209
// return parseInt(Utils.bufReadHex(buf, 32, 36), 16);
199210
}
211+
212+
static generateSeed(): SodiumRNGSeed {
213+
return [
214+
'sodium',
215+
// 32 bits each, 128 bits total (16 bytes)
216+
Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') +
217+
Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') +
218+
Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') +
219+
Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0'),
220+
];
221+
}
200222
}
201223

202224
/**
@@ -210,8 +232,8 @@ export class Gen5RNG implements RNG {
210232
this.seed = [...seed || Gen5RNG.generateSeed()];
211233
}
212234

213-
getSeed() {
214-
return this.seed;
235+
getSeed(): PRNGSeed {
236+
return this.seed.join(',') as PRNGSeed;
215237
}
216238

217239
next(): number {

‎sim/tools/exhaustive-runner.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ export class ExhaustiveRunner {
5959
constructor(options: ExhaustiveRunnerOptions) {
6060
this.format = options.format;
6161
this.cycles = options.cycles || ExhaustiveRunner.DEFAULT_CYCLES;
62-
this.prng = (options.prng && !Array.isArray(options.prng)) ?
63-
options.prng : new PRNG(options.prng);
62+
this.prng = PRNG.get(options.prng);
6463
this.log = !!options.log;
6564
this.maxGames = options.maxGames;
6665
this.maxFailures = options.maxFailures || ExhaustiveRunner.MAX_FAILURES;
@@ -100,7 +99,7 @@ export class ExhaustiveRunner {
10099
this.failures++;
101100
console.error(
102101
`\n\nRun \`node tools/simulate exhaustive --cycles=${this.cycles} ` +
103-
`--format=${this.format} --seed=${seed.join()}\`:\n`,
102+
`--format=${this.format} --seed=${seed}\`:\n`,
104103
err
105104
);
106105
}
@@ -198,7 +197,7 @@ class TeamGenerator {
198197
signatures: Map<string, {item: string, move?: string}[]>
199198
) {
200199
this.dex = dex;
201-
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
200+
this.prng = PRNG.get(prng);
202201
this.pools = pools;
203202
this.signatures = signatures;
204203

‎sim/tools/multi-random-runner.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ export class MultiRandomRunner {
4646

4747
this.totalGames = options.totalGames;
4848

49-
this.prng = (options.prng && !Array.isArray(options.prng)) ?
50-
options.prng : new PRNG(options.prng);
49+
this.prng = PRNG.get(options.prng);
5150
this.options.prng = this.prng;
5251

5352
this.format = options.format;
@@ -75,7 +74,7 @@ export class MultiRandomRunner {
7574
const game = new Runner({format, ...this.options}).run().catch(err => {
7675
failures++;
7776
console.error(
78-
`Run \`node tools/simulate multi 1 --format=${format} --seed=${seed.join()}\` ` +
77+
`Run \`node tools/simulate multi 1 --format=${format} --seed=${seed}\` ` +
7978
`to debug (optionally with \`--output\` and/or \`--input\` for more info):\n`,
8079
err
8180
);

‎sim/tools/random-player-ai.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class RandomPlayerAI extends BattlePlayer {
2323
super(playerStream, debug);
2424
this.move = options.move || 1.0;
2525
this.mega = options.mega || 0;
26-
this.prng = options.seed && !Array.isArray(options.seed) ? options.seed : new PRNG(options.seed);
26+
this.prng = PRNG.get(options.seed);
2727
}
2828

2929
receiveError(error: Error) {

‎sim/tools/runner.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ export class Runner {
5858
constructor(options: RunnerOptions) {
5959
this.format = options.format;
6060

61-
this.prng = (options.prng && !Array.isArray(options.prng)) ?
62-
options.prng : new PRNG(options.prng);
61+
this.prng = PRNG.get(options.prng);
6362
this.p1options = {...Runner.AI_OPTIONS, ...options.p1options};
6463
this.p2options = {...Runner.AI_OPTIONS, ...options.p2options};
6564
this.p3options = {...Runner.AI_OPTIONS, ...options.p3options};
@@ -144,7 +143,7 @@ export class Runner {
144143
this.prng.random(2 ** 16),
145144
this.prng.random(2 ** 16),
146145
this.prng.random(2 ** 16),
147-
];
146+
].join(',') as PRNGSeed;
148147
}
149148

150149
private getPlayerSpec(name: string, options: AIOptions) {

‎test/common.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function capitalize(word) {
1616
/**
1717
* The default random number generator seed used if one is not given.
1818
*/
19-
const DEFAULT_SEED = [0x09917, 0x06924, 0x0e1c8, 0x06af0];
19+
const DEFAULT_SEED = 'gen5,99176924e1c86af0';
2020

2121
class TestTools {
2222
constructor(mod = 'base') {

‎test/random-battles/tools.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function testTeam(options, test) {
110110

111111
const generator = Teams.getGenerator(options.format, [0, 0, 0, 0]);
112112
for (let i = 0; i < rounds; i++) {
113-
generator.setSeed(options.seed || [i, i, i, i]);
113+
generator.setSeed(options.seed || [i, i, i, i].join(','));
114114
const team = generator.getTeam();
115115
test(team);
116116
}

‎test/sim/misc/prng.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const PRNG = require('../../../dist/sim/prng').PRNG;
44
const assert = require('../../assert');
55

6-
const testSeed = ['sodium', '00000001000000020000000300000004'];
6+
const testSeed = 'sodium,00000001000000020000000300000004';
77

88
describe(`PRNG`, function () {
99
it("should always generate the same results off the same seed", function () {

‎test/sim/misc/state.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ describe('State', function () {
2525
describe('Battles', function () {
2626
it('should be able to be serialized and deserialized without affecting functionality (slow)', function () {
2727
this.timeout(5000);
28-
const control = common.createBattle({seed: ['sodium', '00000001000000020000000300000004']}, TEAMS);
29-
let test = common.createBattle({seed: ['sodium', '00000001000000020000000300000004']}, TEAMS);
28+
const control = common.createBattle({seed: 'sodium,00000001000000020000000300000004'}, TEAMS);
29+
let test = common.createBattle({seed: 'sodium,00000001000000020000000300000004'}, TEAMS);
3030

3131
while (!(control.ended || test.ended)) {
3232
control.makeChoices();

‎test/sim/moves/thrash.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('Thrash [Gen 1]', function () {
2525
});
2626

2727
it("Four turn Thrash", function () {
28-
battle = common.gen(1).createBattle({seed: [1, 1, 1, 1]});
28+
battle = common.gen(1).createBattle({seed: 'gen5,0001000100010001'});
2929
battle.setPlayer('p1', {team: [{species: "Nidoking", moves: ['thrash']}]});
3030
battle.setPlayer('p2', {team: [{species: "Golem", moves: ['splash']}]});
3131
const nidoking = battle.p1.active[0];

0 commit comments

Comments
 (0)
Please sign in to comment.