Skip to content

Commit d3e60b3

Browse files
mia-pi-gitZarel
andauthored
Sim: Use a CSPRNG (#10806)
* Sim: Use a CSPRNG * Add test * fix test prng * move prng test to others * fix slight hack * tf? * Fuck this * fucking lol * fix crap * i'm going to kill someone * i hate state * fix test * Good work genius * typo * Fix exportinputlog * Refactor for inputlog backwards compatibility This is a pretty major refactor which is mostly unrelated to the feature, but it does make the code a lot simpler. * Readability pass * Readability (again) * Remove sodium-native dependency * Refactor to serialize seeds in hex strings (Also removes the Buffer dependency from PRNG, and slightly improves comments.) * Apparently << is 32-bit signed * Readability --------- Co-authored-by: Guangcong Luo <guangcongluo@gmail.com>
1 parent 66792c9 commit d3e60b3

27 files changed

+276
-107
lines changed

data/cg-teams.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ export default class TeamGenerator {
10281028

10291029
const totalWeight = weights.reduce((a, b) => a + b, 0);
10301030

1031-
let randomWeight = this.prng.next(0, totalWeight);
1031+
let randomWeight = this.prng.random(0, totalWeight);
10321032
for (let i = 0; i < choices.length; i++) {
10331033
randomWeight -= weights[i];
10341034
if (randomWeight < 0) {
@@ -1043,6 +1043,6 @@ export default class TeamGenerator {
10431043
}
10441044

10451045
setSeed(seed: PRNGSeed) {
1046-
this.prng.seed = seed;
1046+
this.prng.setSeed(seed);
10471047
}
10481048
}

data/mods/gen2/moves.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
590590
this.debug('Pursuit start');
591591
let alreadyAdded = false;
592592
for (const source of this.effectState.sources) {
593-
if (source.speed < pokemon.speed || (source.speed === pokemon.speed && this.random(2) === 0)) {
593+
if (source.speed < pokemon.speed || (source.speed === pokemon.speed && this.randomChance(1, 2))) {
594594
// Destiny Bond ends if the switch action "outspeeds" the attacker, regardless of host
595595
pokemon.removeVolatile('destinybond');
596596
}

data/mods/gen5/conditions.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ export const Conditions: import('../../../sim/dex-conditions').ModdedConditionDa
3232
// However, just in case, use 1 if it is undefined.
3333
const counter = this.effectState.counter || 1;
3434
if (counter >= 256) {
35-
// 2^32 - special-cased because Battle.random(n) can't handle n > 2^16 - 1
36-
return (this.random() * 4294967296 < 1);
35+
return this.randomChance(1, 2 ** 32);
3736
}
3837
this.debug("Success chance: " + Math.round(100 / counter) + "%");
3938
return this.randomChance(1, counter);

data/mods/gen9ssb/moves.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1248,7 +1248,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
12481248
target.clearBoosts();
12491249
this.add('-clearboost', target);
12501250
target.addVolatile('protect');
1251-
const set = Math.floor(Math.random() * 4);
1251+
const set = this.random(4);
12521252
const newMoves = [];
12531253
let role = '';
12541254
switch (set) {
@@ -2608,7 +2608,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
26082608
const spd = target.getStat('spd', false, true);
26092609
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
26102610
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
2611-
if (physical > special || (physical === special && this.random(2) === 0)) {
2611+
if (physical > special || (physical === special && this.randomChance(1, 2))) {
26122612
move.category = 'Physical';
26132613
move.flags.contact = 1;
26142614
}

data/moves.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -7352,7 +7352,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
73527352
isMax: "Snorlax",
73537353
self: {
73547354
onHit(source) {
7355-
if (this.random(2) === 0) return;
7355+
if (this.randomChance(1, 2)) return;
73567356
for (const pokemon of source.alliesAndSelf()) {
73577357
if (pokemon.item) continue;
73587358

@@ -7448,12 +7448,12 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
74487448
isMax: "Grimmsnarl",
74497449
onHit(target) {
74507450
if (target.status || !target.runStatusImmunity('slp')) return;
7451-
if (this.random(2) === 0) return;
7451+
if (this.randomChance(1, 2)) return;
74527452
target.addVolatile('yawn');
74537453
},
74547454
onAfterSubDamage(damage, target) {
74557455
if (target.status || !target.runStatusImmunity('slp')) return;
7456-
if (this.random(2) === 0) return;
7456+
if (this.randomChance(1, 2)) return;
74577457
target.addVolatile('yawn');
74587458
},
74597459
secondary: null,
@@ -16812,7 +16812,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
1681216812
const spd = target.getStat('spd', false, true);
1681316813
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
1681416814
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
16815-
if (physical > special || (physical === special && this.random(2) === 0)) {
16815+
if (physical > special || (physical === special && this.randomChance(1, 2))) {
1681616816
move.category = 'Physical';
1681716817
move.flags.contact = 1;
1681816818
}

data/random-battles/gen1/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class RandomGen1Teams extends RandomGen2Teams {
107107
this.enforceNoDirectCustomBanlistChanges();
108108

109109
// Get what we need ready.
110-
const seed = this.prng.seed;
110+
const seed = this.prng.getSeed();
111111
const ruleTable = this.dex.formats.getRuleTable(this.format);
112112
const pokemon: RandomTeamsTypes.RandomSet[] = [];
113113

data/random-battles/gen3/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ export class RandomGen3Teams extends RandomGen4Teams {
639639
randomTeam() {
640640
this.enforceNoDirectCustomBanlistChanges();
641641

642-
const seed = this.prng.seed;
642+
const seed = this.prng.getSeed();
643643
const ruleTable = this.dex.formats.getRuleTable(this.format);
644644
const pokemon: RandomTeamsTypes.RandomSet[] = [];
645645

data/random-battles/gen5/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ export class RandomGen5Teams extends RandomGen6Teams {
846846
randomTeam() {
847847
this.enforceNoDirectCustomBanlistChanges();
848848

849-
const seed = this.prng.seed;
849+
const seed = this.prng.getSeed();
850850
const ruleTable = this.dex.formats.getRuleTable(this.format);
851851
const pokemon: RandomTeamsTypes.RandomSet[] = [];
852852

data/random-battles/gen7/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,7 @@ export class RandomGen7Teams extends RandomGen8Teams {
11821182
randomTeam() {
11831183
this.enforceNoDirectCustomBanlistChanges();
11841184

1185-
const seed = this.prng.seed;
1185+
const seed = this.prng.getSeed();
11861186
const ruleTable = this.dex.formats.getRuleTable(this.format);
11871187
const pokemon: RandomTeamsTypes.RandomSet[] = [];
11881188

data/random-battles/gen8/teams.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export class RandomGen8Teams {
270270
}
271271

272272
random(m?: number, n?: number) {
273-
return this.prng.next(m, n);
273+
return this.prng.random(m, n);
274274
}
275275

276276
/**
@@ -2479,7 +2479,7 @@ export class RandomGen8Teams {
24792479
randomTeam() {
24802480
this.enforceNoDirectCustomBanlistChanges();
24812481

2482-
const seed = this.prng.seed;
2482+
const seed = this.prng.getSeed();
24832483
const ruleTable = this.dex.formats.getRuleTable(this.format);
24842484
const pokemon: RandomTeamsTypes.RandomSet[] = [];
24852485

@@ -3112,7 +3112,7 @@ export class RandomGen8Teams {
31123112
for (const speciesName of pokemonPool) {
31133113
const sortObject = {
31143114
speciesName: speciesName,
3115-
score: Math.pow(this.prng.next(), 1 / this.randomBSSFactorySets[speciesName].usage),
3115+
score: Math.pow(this.prng.random(), 1 / this.randomBSSFactorySets[speciesName].usage),
31163116
};
31173117
shuffledSpecies.push(sortObject);
31183118
}

data/random-battles/gen9/teams.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export class RandomTeams {
279279
}
280280

281281
random(m?: number, n?: number) {
282-
return this.prng.next(m, n);
282+
return this.prng.random(m, n);
283283
}
284284

285285
/**
@@ -1632,7 +1632,7 @@ export class RandomTeams {
16321632
randomTeam() {
16331633
this.enforceNoDirectCustomBanlistChanges();
16341634

1635-
const seed = this.prng.seed;
1635+
const seed = this.prng.getSeed();
16361636
const ruleTable = this.dex.formats.getRuleTable(this.format);
16371637
const pokemon: RandomTeamsTypes.RandomSet[] = [];
16381638

@@ -2551,7 +2551,7 @@ export class RandomTeams {
25512551
for (const speciesName of pokemonPool) {
25522552
const sortObject = {
25532553
speciesName,
2554-
score: Math.pow(this.prng.next(), 1 / this.randomFactorySets[this.factoryTier][speciesName].weight),
2554+
score: Math.pow(this.prng.random(), 1 / this.randomFactorySets[this.factoryTier][speciesName].weight),
25552555
};
25562556
shuffledSpecies.push(sortObject);
25572557
}
@@ -2847,7 +2847,7 @@ export class RandomTeams {
28472847
for (const speciesName of pokemonPool) {
28482848
const sortObject = {
28492849
speciesName,
2850-
score: Math.pow(this.prng.next(), 1 / this.randomBSSFactorySets[speciesName].weight),
2850+
score: Math.pow(this.prng.random(), 1 / this.randomBSSFactorySets[speciesName].weight),
28512851
};
28522852
shuffledSpecies.push(sortObject);
28532853
}

data/random-battles/gen9baby/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ export class RandomBabyTeams extends RandomTeams {
669669
randomBabyTeam() {
670670
this.enforceNoDirectCustomBanlistChanges();
671671

672-
const seed = this.prng.seed;
672+
const seed = this.prng.getSeed();
673673
const ruleTable = this.dex.formats.getRuleTable(this.format);
674674
const pokemon: RandomTeamsTypes.RandomSet[] = [];
675675

data/random-battles/gen9cap/teams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class RandomCAPTeams extends RandomTeams {
185185
randomTeam() {
186186
this.enforceNoDirectCustomBanlistChanges();
187187

188-
const seed = this.prng.seed;
188+
const seed = this.prng.getSeed();
189189
const ruleTable = this.dex.formats.getRuleTable(this.format);
190190
const pokemon: RandomTeamsTypes.RandomSet[] = [];
191191

data/rulesets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2543,7 +2543,7 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
25432543
const spd = target.getStat('spd', false, true);
25442544
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
25452545
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
2546-
if (physical > special || (physical === special && this.random(2) === 0)) {
2546+
if (physical > special || (physical === special && this.randomChance(1, 2))) {
25472547
move.category = 'Physical';
25482548
move.flags.contact = 1;
25492549
}

lib/utils.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,21 @@ export function formatSQLArray(arr: unknown[], args?: unknown[]) {
413413
return [...'?'.repeat(arr.length)].join(', ');
414414
}
415415

416+
export function bufFromHex(hex: string) {
417+
const buf = new Uint8Array(Math.ceil(hex.length / 2));
418+
bufWriteHex(buf, hex);
419+
return buf;
420+
}
421+
export function bufWriteHex(buf: Uint8Array, hex: string, offset = 0) {
422+
const size = Math.ceil(hex.length / 2);
423+
for (let i = 0; i < size; i++) {
424+
buf[offset + i] = parseInt(hex.slice(i * 2, i * 2 + 2).padEnd(2, '0'), 16);
425+
}
426+
}
427+
export function bufReadHex(buf: Uint8Array, start = 0, end?: number) {
428+
return [...buf.slice(start, end)].map(val => val.toString(16).padStart(2, '0')).join('');
429+
}
430+
416431
export class Multiset<T> extends Map<T, number> {
417432
get(key: T) {
418433
return super.get(key) ?? 0;
@@ -436,5 +451,7 @@ export const Utils = {
436451
shuffle, deepClone, clearRequireCache,
437452
randomElement, forceWrap, splitFirst,
438453
stripHTML, visualize, getString,
439-
escapeRegex, formatSQLArray, Multiset,
454+
escapeRegex, formatSQLArray,
455+
bufFromHex, bufReadHex, bufWriteHex,
456+
Multiset,
440457
};

package-lock.json

+33-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"preact-render-to-string": "^5.1.19",
1111
"probe-image-size": "^7.2.3",
1212
"sockjs": "^0.3.21",
13-
"source-map-support": "^0.5.21"
13+
"source-map-support": "^0.5.21",
14+
"ts-chacha20": "^1.2.0"
1415
},
1516
"optionalDependencies": {
1617
"better-sqlite3": "^7.6.2",
@@ -72,6 +73,7 @@
7273
"@types/nodemailer": "^6.4.4",
7374
"@types/pg": "^8.6.5",
7475
"@types/sockjs": "^0.3.33",
76+
"@types/sodium-native": "^2.3.9",
7577
"@typescript-eslint/eslint-plugin": "^5.8.0",
7678
"@typescript-eslint/parser": "^5.8.0",
7779
"eslint": "8.5.0",

sim/battle-stream.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,12 @@ 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(Number) as PRNGSeed : null;
139+
const seed = message ? message.split(',').map(
140+
n => /[0-9]/.test(n.charAt(0)) ? Number(n) : n
141+
) as PRNGSeed : null;
140142
this.battle!.resetRNG(seed);
141143
// could go inside resetRNG, but this makes using it in `eval` slightly less buggy
142-
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.seed.join(',')}`);
144+
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed().join(',')}`);
143145
break;
144146
case 'tiebreak':
145147
this.battle!.tiebreak();

sim/battle.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export class Battle {
222222
(format.playerCount > 2 || this.gameType === 'doubles') ? 2 :
223223
1;
224224
this.prng = options.prng || new PRNG(options.seed || undefined);
225-
this.prngSeed = this.prng.startingSeed.slice() as PRNGSeed;
225+
this.prngSeed = this.prng.startingSeed;
226226
this.rated = options.rated || !!options.rated;
227227
this.reportExactHP = !!format.debug;
228228
this.reportPercentages = false;
@@ -273,7 +273,7 @@ export class Battle {
273273
this.send = options.send || (() => {});
274274

275275
const inputOptions: {formatid: ID, seed: PRNGSeed, rated?: string | true} = {
276-
formatid: options.formatid, seed: this.prng.seed,
276+
formatid: options.formatid, seed: this.prngSeed,
277277
};
278278
if (this.rated) inputOptions.rated = this.rated;
279279
if (typeof __version !== 'undefined') {
@@ -340,7 +340,7 @@ export class Battle {
340340
}
341341

342342
random(m?: number, n?: number) {
343-
return this.prng.next(m, n);
343+
return this.prng.random(m, n);
344344
}
345345

346346
randomChance(numerator: number, denominator: number) {
@@ -353,7 +353,7 @@ export class Battle {
353353
}
354354

355355
/** Note that passing `undefined` resets to the starting seed, but `null` will roll a new seed */
356-
resetRNG(seed: PRNGSeed | null = this.prng.startingSeed) {
356+
resetRNG(seed: PRNGSeed | null = this.prngSeed) {
357357
this.prng = new PRNG(seed);
358358
this.add('message', "The battle's RNG was reset.");
359359
}

sim/pokemon.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export class Pokemon {
317317
set.level = this.battle.clampIntRange(set.adjustLevel || set.level || 100, 1, 9999);
318318
this.level = set.level;
319319
const genders: {[key: string]: GenderName} = {M: 'M', F: 'F', N: 'N'};
320-
this.gender = genders[set.gender] || this.species.gender || (this.battle.random() * 2 < 1 ? 'M' : 'F');
320+
this.gender = genders[set.gender] || this.species.gender || (this.battle.random(2) ? 'F' : 'M');
321321
if (this.gender === 'N') this.gender = '';
322322
this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255;
323323
this.pokeball = this.set.pokeball || 'pokeball';

0 commit comments

Comments
 (0)