Skip to content

Commit

Permalink
PRNGSeed is now a string
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Zarel committed Jan 15, 2025
1 parent ec7332b commit 8483236
Show file tree
Hide file tree
Showing 15 changed files with 62 additions and 46 deletions.
2 changes: 1 addition & 1 deletion data/cg-teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions data/random-battles/gen8/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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[] {
Expand Down
4 changes: 2 additions & 2 deletions data/random-battles/gen9/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
Expand Down Expand Up @@ -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[] {
Expand Down
2 changes: 1 addition & 1 deletion pokemon-showdown
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 2 additions & 5 deletions sim/battle-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,9 @@ export class BattleStream extends Streams.ObjectReadWriteStream<string> {
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();
Expand Down
58 changes: 40 additions & 18 deletions sim/prng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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() {
Expand All @@ -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'),
];
}
}

/**
Expand All @@ -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 {
Expand Down
7 changes: 3 additions & 4 deletions sim/tools/exhaustive-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}
Expand Down Expand Up @@ -198,7 +197,7 @@ class TeamGenerator {
signatures: Map<string, {item: string, move?: string}[]>
) {
this.dex = dex;
this.prng = prng && !Array.isArray(prng) ? prng : new PRNG(prng);
this.prng = PRNG.get(prng);
this.pools = pools;
this.signatures = signatures;

Expand Down
5 changes: 2 additions & 3 deletions sim/tools/multi-random-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
Expand Down
2 changes: 1 addition & 1 deletion sim/tools/random-player-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 2 additions & 3 deletions sim/tools/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
2 changes: 1 addition & 1 deletion test/random-battles/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion test/sim/misc/prng.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
4 changes: 2 additions & 2 deletions test/sim/misc/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion test/sim/moves/thrash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down

0 comments on commit 8483236

Please sign in to comment.