From fde2b1187f5cbc96b5626fe539bcdd7fde07a147 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 15 Jan 2025 15:51:28 -0800 Subject: [PATCH] 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. --- data/cg-teams.ts | 2 +- data/random-battles/gen8/teams.ts | 4 +-- data/random-battles/gen9/teams.ts | 4 +-- pokemon-showdown | 2 +- sim/battle-stream.ts | 7 ++-- sim/prng.ts | 58 +++++++++++++++++++++---------- sim/tools/exhaustive-runner.ts | 7 ++-- sim/tools/multi-random-runner.ts | 5 ++- sim/tools/random-player-ai.ts | 2 +- sim/tools/runner.ts | 5 ++- test/common.js | 2 +- test/random-battles/tools.js | 2 +- test/sim/misc/prng.js | 2 +- test/sim/misc/state.js | 4 +-- test/sim/moves/thrash.js | 2 +- 15 files changed, 62 insertions(+), 46 deletions(-) diff --git a/data/cg-teams.ts b/data/cg-teams.ts index 026d6ba51089..826833072007 100644 --- a/data/cg-teams.ts +++ b/data/cg-teams.ts @@ -111,7 +111,7 @@ export default class TeamGenerator { this.dex = Dex.forFormat(format); this.format = Dex.formats.get(format); this.teamSize = this.format.ruleTable?.maxTeamSize || 6; - this.prng = seed instanceof PRNG ? seed : new PRNG(seed); + this.prng = PRNG.get(seed); this.itemPool = this.dex.items.all().filter(i => i.exists && i.isNonstandard !== 'Past' && !i.isPokeball); this.specialItems = {}; for (const i of this.itemPool) { diff --git a/data/random-battles/gen8/teams.ts b/data/random-battles/gen8/teams.ts index b403bd38bd21..0448cbbb53ad 100644 --- a/data/random-battles/gen8/teams.ts +++ b/data/random-battles/gen8/teams.ts @@ -138,7 +138,7 @@ export class RandomGen8Teams { this.factoryTier = ''; this.format = format; - this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); + this.prng = PRNG.get(prng); this.moveEnforcementCheckers = { screens: (movePool, moves, abilities, types, counter, species, teamDetails) => { @@ -243,7 +243,7 @@ export class RandomGen8Teams { } setSeed(prng?: PRNG | PRNGSeed) { - this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); + this.prng = PRNG.get(prng); } getTeam(options?: PlayerOptions | null): PokemonSet[] { diff --git a/data/random-battles/gen9/teams.ts b/data/random-battles/gen9/teams.ts index f2ec331f626c..e372ff0e5ab1 100644 --- a/data/random-battles/gen9/teams.ts +++ b/data/random-battles/gen9/teams.ts @@ -195,7 +195,7 @@ export class RandomTeams { this.factoryTier = ''; this.format = format; - this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); + this.prng = PRNG.get(prng); this.moveEnforcementCheckers = { Bug: (movePool, moves, abilities, types, counter) => ( @@ -252,7 +252,7 @@ export class RandomTeams { } setSeed(prng?: PRNG | PRNGSeed) { - this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); + this.prng = PRNG.get(prng); } getTeam(options?: PlayerOptions | null): PokemonSet[] { diff --git a/pokemon-showdown b/pokemon-showdown index 6a96085a4090..291a446ca1dc 100755 --- a/pokemon-showdown +++ b/pokemon-showdown @@ -105,7 +105,7 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { { ensureBuilt(); var Teams = require('./dist/sim/teams.js').Teams; - var seed = process.argv[4] ? process.argv[4].split(',').map(Number) : undefined; + var seed = process.argv[4] || undefined; console.log(Teams.pack(Teams.generate(process.argv[3], {seed}))); } break; diff --git a/sim/battle-stream.ts b/sim/battle-stream.ts index ed96d384d3b5..a2098ce0cd59 100644 --- a/sim/battle-stream.ts +++ b/sim/battle-stream.ts @@ -136,12 +136,9 @@ export class BattleStream extends Streams.ObjectReadWriteStream { this.battle!.inputLog.push(`>forcelose ${message}`); break; case 'reseed': - const seed = message ? message.split(',').map( - n => /[0-9]/.test(n.charAt(0)) ? Number(n) : n - ) as PRNGSeed : null; - this.battle!.resetRNG(seed); + this.battle!.resetRNG(message as PRNGSeed); // could go inside resetRNG, but this makes using it in `eval` slightly less buggy - this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed().join(',')}`); + this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed()}`); break; case 'tiebreak': this.battle!.tiebreak(); diff --git a/sim/prng.ts b/sim/prng.ts index 503f75dff6e2..3cea68cc5e19 100644 --- a/sim/prng.ts +++ b/sim/prng.ts @@ -15,7 +15,7 @@ import {Chacha20} from 'ts-chacha20'; import {Utils} from '../lib/utils'; -export type PRNGSeed = SodiumRNGSeed | Gen5RNGSeed; +export type PRNGSeed = `${'sodium' | 'gen5' | number},${string}`; export type SodiumRNGSeed = ['sodium', string]; /** 64-bit big-endian [high -> low] int */ export type Gen5RNGSeed = [number, number, number, number]; @@ -44,15 +44,27 @@ export class PRNG { /** Creates a new source of randomness for the given seed. */ constructor(seed: PRNGSeed | null = null, initialSeed?: PRNGSeed) { if (!seed) seed = PRNG.generateSeed(); - this.startingSeed = initialSeed || [...seed]; // make a copy + if (Array.isArray(seed)) { + // compat for old inputlogs + seed = seed.join(',') as PRNGSeed; + } + if (typeof seed !== 'string') { + throw new Error(`PRNG: Seed ${seed} must be a string`); + } + this.startingSeed = initialSeed ?? seed; this.setSeed(seed); } setSeed(seed: PRNGSeed) { - if (seed[0] === 'sodium') { - this.rng = new SodiumRNG(seed); + if (seed.startsWith('sodium,')) { + this.rng = new SodiumRNG(seed.split(',') as SodiumRNGSeed); + } else if (seed.startsWith('gen5,')) { + const gen5Seed = [seed.slice(5, 9), seed.slice(9, 13), seed.slice(13, 17), seed.slice(17, 21)]; + this.rng = new Gen5RNG(gen5Seed.map(n => parseInt(n, 16)) as Gen5RNGSeed); + } else if (/[0-9]/.test(seed.charAt(0))) { + this.rng = new Gen5RNG(seed.split(',').map(Number) as Gen5RNGSeed); } else { - this.rng = new Gen5RNG(seed as Gen5RNGSeed); + throw new Error(`Unrecognized RNG seed ${seed}`); } } getSeed(): PRNGSeed { @@ -145,15 +157,14 @@ export class PRNG { } } - static generateSeed(): SodiumRNGSeed { - return [ - 'sodium', - // 32 bits each, 128 bits total (16 bytes) - Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + - Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + - Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + - Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0'), - ]; + static generateSeed(): PRNGSeed { + return PRNG.convertSeed(SodiumRNG.generateSeed()); + } + static convertSeed(seed: SodiumRNGSeed | Gen5RNGSeed): PRNGSeed { + return seed.join(',') as PRNGSeed; + } + static get(prng?: PRNG | PRNGSeed | null) { + return prng && typeof prng !== 'string' && !Array.isArray(prng) ? prng : new PRNG(prng as PRNGSeed); } } @@ -180,8 +191,8 @@ export class SodiumRNG implements RNG { Utils.bufWriteHex(seedBuf, seed[1].padEnd(64, '0')); this.seed = seedBuf; } - getSeed(): SodiumRNGSeed { - return ['sodium', Utils.bufReadHex(this.seed)]; + getSeed(): PRNGSeed { + return `sodium,${Utils.bufReadHex(this.seed)}`; } next() { @@ -197,6 +208,17 @@ export class SodiumRNG implements RNG { // alternative, probably slower (TODO: benchmark) // return parseInt(Utils.bufReadHex(buf, 32, 36), 16); } + + static generateSeed(): SodiumRNGSeed { + return [ + 'sodium', + // 32 bits each, 128 bits total (16 bytes) + Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + + Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + + Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0') + + Math.trunc(Math.random() * 2 ** 32).toString(16).padStart(8, '0'), + ]; + } } /** @@ -210,8 +232,8 @@ export class Gen5RNG implements RNG { this.seed = [...seed || Gen5RNG.generateSeed()]; } - getSeed() { - return this.seed; + getSeed(): PRNGSeed { + return this.seed.join(',') as PRNGSeed; } next(): number { diff --git a/sim/tools/exhaustive-runner.ts b/sim/tools/exhaustive-runner.ts index 161f7c8d7fe1..32e61ff028b3 100644 --- a/sim/tools/exhaustive-runner.ts +++ b/sim/tools/exhaustive-runner.ts @@ -59,8 +59,7 @@ export class ExhaustiveRunner { constructor(options: ExhaustiveRunnerOptions) { this.format = options.format; this.cycles = options.cycles || ExhaustiveRunner.DEFAULT_CYCLES; - this.prng = (options.prng && !Array.isArray(options.prng)) ? - options.prng : new PRNG(options.prng); + this.prng = PRNG.get(options.prng); this.log = !!options.log; this.maxGames = options.maxGames; this.maxFailures = options.maxFailures || ExhaustiveRunner.MAX_FAILURES; @@ -100,7 +99,7 @@ export class ExhaustiveRunner { this.failures++; console.error( `\n\nRun \`node tools/simulate exhaustive --cycles=${this.cycles} ` + - `--format=${this.format} --seed=${seed.join()}\`:\n`, + `--format=${this.format} --seed=${seed}\`:\n`, err ); } @@ -198,7 +197,7 @@ class TeamGenerator { signatures: Map ) { this.dex = dex; - this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng); + this.prng = PRNG.get(prng); this.pools = pools; this.signatures = signatures; diff --git a/sim/tools/multi-random-runner.ts b/sim/tools/multi-random-runner.ts index 70b373a4ed2d..c9eff1c87e82 100644 --- a/sim/tools/multi-random-runner.ts +++ b/sim/tools/multi-random-runner.ts @@ -46,8 +46,7 @@ export class MultiRandomRunner { this.totalGames = options.totalGames; - this.prng = (options.prng && !Array.isArray(options.prng)) ? - options.prng : new PRNG(options.prng); + this.prng = PRNG.get(options.prng); this.options.prng = this.prng; this.format = options.format; @@ -75,7 +74,7 @@ export class MultiRandomRunner { const game = new Runner({format, ...this.options}).run().catch(err => { failures++; console.error( - `Run \`node tools/simulate multi 1 --format=${format} --seed=${seed.join()}\` ` + + `Run \`node tools/simulate multi 1 --format=${format} --seed=${seed}\` ` + `to debug (optionally with \`--output\` and/or \`--input\` for more info):\n`, err ); diff --git a/sim/tools/random-player-ai.ts b/sim/tools/random-player-ai.ts index fde8a91e372a..b138b30dadb9 100644 --- a/sim/tools/random-player-ai.ts +++ b/sim/tools/random-player-ai.ts @@ -23,7 +23,7 @@ export class RandomPlayerAI extends BattlePlayer { super(playerStream, debug); this.move = options.move || 1.0; this.mega = options.mega || 0; - this.prng = options.seed && !Array.isArray(options.seed) ? options.seed : new PRNG(options.seed); + this.prng = PRNG.get(options.seed); } receiveError(error: Error) { diff --git a/sim/tools/runner.ts b/sim/tools/runner.ts index 11a86680739b..f0069c33328e 100644 --- a/sim/tools/runner.ts +++ b/sim/tools/runner.ts @@ -58,8 +58,7 @@ export class Runner { constructor(options: RunnerOptions) { this.format = options.format; - this.prng = (options.prng && !Array.isArray(options.prng)) ? - options.prng : new PRNG(options.prng); + this.prng = PRNG.get(options.prng); this.p1options = {...Runner.AI_OPTIONS, ...options.p1options}; this.p2options = {...Runner.AI_OPTIONS, ...options.p2options}; this.p3options = {...Runner.AI_OPTIONS, ...options.p3options}; @@ -144,7 +143,7 @@ export class Runner { this.prng.random(2 ** 16), this.prng.random(2 ** 16), this.prng.random(2 ** 16), - ]; + ].join(',') as PRNGSeed; } private getPlayerSpec(name: string, options: AIOptions) { diff --git a/test/common.js b/test/common.js index 1ae440aedf9f..7de77090ea0e 100644 --- a/test/common.js +++ b/test/common.js @@ -16,7 +16,7 @@ function capitalize(word) { /** * The default random number generator seed used if one is not given. */ -const DEFAULT_SEED = [0x09917, 0x06924, 0x0e1c8, 0x06af0]; +const DEFAULT_SEED = 'gen5,99176924e1c86af0'; class TestTools { constructor(mod = 'base') { diff --git a/test/random-battles/tools.js b/test/random-battles/tools.js index 4c0d97ba9f2a..a880c53e17d3 100644 --- a/test/random-battles/tools.js +++ b/test/random-battles/tools.js @@ -110,7 +110,7 @@ function testTeam(options, test) { const generator = Teams.getGenerator(options.format, [0, 0, 0, 0]); for (let i = 0; i < rounds; i++) { - generator.setSeed(options.seed || [i, i, i, i]); + generator.setSeed(options.seed || [i, i, i, i].join(',')); const team = generator.getTeam(); test(team); } diff --git a/test/sim/misc/prng.js b/test/sim/misc/prng.js index 3a67637c8f1c..7adfc5bcb095 100644 --- a/test/sim/misc/prng.js +++ b/test/sim/misc/prng.js @@ -3,7 +3,7 @@ const PRNG = require('../../../dist/sim/prng').PRNG; const assert = require('../../assert'); -const testSeed = ['sodium', '00000001000000020000000300000004']; +const testSeed = 'sodium,00000001000000020000000300000004'; describe(`PRNG`, function () { it("should always generate the same results off the same seed", function () { diff --git a/test/sim/misc/state.js b/test/sim/misc/state.js index 670a61a9eed6..7e9a24e5ff39 100644 --- a/test/sim/misc/state.js +++ b/test/sim/misc/state.js @@ -25,8 +25,8 @@ describe('State', function () { describe('Battles', function () { it('should be able to be serialized and deserialized without affecting functionality (slow)', function () { this.timeout(5000); - const control = common.createBattle({seed: ['sodium', '00000001000000020000000300000004']}, TEAMS); - let test = common.createBattle({seed: ['sodium', '00000001000000020000000300000004']}, TEAMS); + const control = common.createBattle({seed: 'sodium,00000001000000020000000300000004'}, TEAMS); + let test = common.createBattle({seed: 'sodium,00000001000000020000000300000004'}, TEAMS); while (!(control.ended || test.ended)) { control.makeChoices(); diff --git a/test/sim/moves/thrash.js b/test/sim/moves/thrash.js index 33ebcf324c38..94ee865a6531 100644 --- a/test/sim/moves/thrash.js +++ b/test/sim/moves/thrash.js @@ -25,7 +25,7 @@ describe('Thrash [Gen 1]', function () { }); it("Four turn Thrash", function () { - battle = common.gen(1).createBattle({seed: [1, 1, 1, 1]}); + battle = common.gen(1).createBattle({seed: 'gen5,0001000100010001'}); battle.setPlayer('p1', {team: [{species: "Nidoking", moves: ['thrash']}]}); battle.setPlayer('p2', {team: [{species: "Golem", moves: ['splash']}]}); const nidoking = battle.p1.active[0];