Skip to content

Commit

Permalink
Sim: Use a CSPRNG (#10806)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
mia-pi-git and Zarel authored Jan 11, 2025
1 parent 66792c9 commit d3e60b3
Show file tree
Hide file tree
Showing 27 changed files with 276 additions and 107 deletions.
4 changes: 2 additions & 2 deletions data/cg-teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ export default class TeamGenerator {

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

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

setSeed(seed: PRNGSeed) {
this.prng.seed = seed;
this.prng.setSeed(seed);
}
}
2 changes: 1 addition & 1 deletion data/mods/gen2/moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
this.debug('Pursuit start');
let alreadyAdded = false;
for (const source of this.effectState.sources) {
if (source.speed < pokemon.speed || (source.speed === pokemon.speed && this.random(2) === 0)) {
if (source.speed < pokemon.speed || (source.speed === pokemon.speed && this.randomChance(1, 2))) {
// Destiny Bond ends if the switch action "outspeeds" the attacker, regardless of host
pokemon.removeVolatile('destinybond');
}
Expand Down
3 changes: 1 addition & 2 deletions data/mods/gen5/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export const Conditions: import('../../../sim/dex-conditions').ModdedConditionDa
// However, just in case, use 1 if it is undefined.
const counter = this.effectState.counter || 1;
if (counter >= 256) {
// 2^32 - special-cased because Battle.random(n) can't handle n > 2^16 - 1
return (this.random() * 4294967296 < 1);
return this.randomChance(1, 2 ** 32);
}
this.debug("Success chance: " + Math.round(100 / counter) + "%");
return this.randomChance(1, counter);
Expand Down
4 changes: 2 additions & 2 deletions data/mods/gen9ssb/moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
target.clearBoosts();
this.add('-clearboost', target);
target.addVolatile('protect');
const set = Math.floor(Math.random() * 4);
const set = this.random(4);
const newMoves = [];
let role = '';
switch (set) {
Expand Down Expand Up @@ -2608,7 +2608,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = {
const spd = target.getStat('spd', false, true);
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
if (physical > special || (physical === special && this.random(2) === 0)) {
if (physical > special || (physical === special && this.randomChance(1, 2))) {
move.category = 'Physical';
move.flags.contact = 1;
}
Expand Down
8 changes: 4 additions & 4 deletions data/moves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7352,7 +7352,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
isMax: "Snorlax",
self: {
onHit(source) {
if (this.random(2) === 0) return;
if (this.randomChance(1, 2)) return;
for (const pokemon of source.alliesAndSelf()) {
if (pokemon.item) continue;

Expand Down Expand Up @@ -7448,12 +7448,12 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
isMax: "Grimmsnarl",
onHit(target) {
if (target.status || !target.runStatusImmunity('slp')) return;
if (this.random(2) === 0) return;
if (this.randomChance(1, 2)) return;
target.addVolatile('yawn');
},
onAfterSubDamage(damage, target) {
if (target.status || !target.runStatusImmunity('slp')) return;
if (this.random(2) === 0) return;
if (this.randomChance(1, 2)) return;
target.addVolatile('yawn');
},
secondary: null,
Expand Down Expand Up @@ -16812,7 +16812,7 @@ export const Moves: import('../sim/dex-moves').MoveDataTable = {
const spd = target.getStat('spd', false, true);
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
if (physical > special || (physical === special && this.random(2) === 0)) {
if (physical > special || (physical === special && this.randomChance(1, 2))) {
move.category = 'Physical';
move.flags.contact = 1;
}
Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class RandomGen1Teams extends RandomGen2Teams {
this.enforceNoDirectCustomBanlistChanges();

// Get what we need ready.
const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen3/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ export class RandomGen3Teams extends RandomGen4Teams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen5/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ export class RandomGen5Teams extends RandomGen6Teams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen7/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1182,7 +1182,7 @@ export class RandomGen7Teams extends RandomGen8Teams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
6 changes: 3 additions & 3 deletions data/random-battles/gen8/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export class RandomGen8Teams {
}

random(m?: number, n?: number) {
return this.prng.next(m, n);
return this.prng.random(m, n);
}

/**
Expand Down Expand Up @@ -2479,7 +2479,7 @@ export class RandomGen8Teams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down Expand Up @@ -3112,7 +3112,7 @@ export class RandomGen8Teams {
for (const speciesName of pokemonPool) {
const sortObject = {
speciesName: speciesName,
score: Math.pow(this.prng.next(), 1 / this.randomBSSFactorySets[speciesName].usage),
score: Math.pow(this.prng.random(), 1 / this.randomBSSFactorySets[speciesName].usage),
};
shuffledSpecies.push(sortObject);
}
Expand Down
8 changes: 4 additions & 4 deletions data/random-battles/gen9/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export class RandomTeams {
}

random(m?: number, n?: number) {
return this.prng.next(m, n);
return this.prng.random(m, n);
}

/**
Expand Down Expand Up @@ -1632,7 +1632,7 @@ export class RandomTeams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down Expand Up @@ -2551,7 +2551,7 @@ export class RandomTeams {
for (const speciesName of pokemonPool) {
const sortObject = {
speciesName,
score: Math.pow(this.prng.next(), 1 / this.randomFactorySets[this.factoryTier][speciesName].weight),
score: Math.pow(this.prng.random(), 1 / this.randomFactorySets[this.factoryTier][speciesName].weight),
};
shuffledSpecies.push(sortObject);
}
Expand Down Expand Up @@ -2847,7 +2847,7 @@ export class RandomTeams {
for (const speciesName of pokemonPool) {
const sortObject = {
speciesName,
score: Math.pow(this.prng.next(), 1 / this.randomBSSFactorySets[speciesName].weight),
score: Math.pow(this.prng.random(), 1 / this.randomBSSFactorySets[speciesName].weight),
};
shuffledSpecies.push(sortObject);
}
Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen9baby/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ export class RandomBabyTeams extends RandomTeams {
randomBabyTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
2 changes: 1 addition & 1 deletion data/random-battles/gen9cap/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class RandomCAPTeams extends RandomTeams {
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();

const seed = this.prng.seed;
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];

Expand Down
2 changes: 1 addition & 1 deletion data/rulesets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2543,7 +2543,7 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
const spd = target.getStat('spd', false, true);
const physical = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * atk) / def) / 50);
const special = Math.floor(Math.floor(Math.floor(Math.floor(2 * pokemon.level / 5 + 2) * 90 * spa) / spd) / 50);
if (physical > special || (physical === special && this.random(2) === 0)) {
if (physical > special || (physical === special && this.randomChance(1, 2))) {
move.category = 'Physical';
move.flags.contact = 1;
}
Expand Down
19 changes: 18 additions & 1 deletion lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,21 @@ export function formatSQLArray(arr: unknown[], args?: unknown[]) {
return [...'?'.repeat(arr.length)].join(', ');
}

export function bufFromHex(hex: string) {
const buf = new Uint8Array(Math.ceil(hex.length / 2));
bufWriteHex(buf, hex);
return buf;
}
export function bufWriteHex(buf: Uint8Array, hex: string, offset = 0) {
const size = Math.ceil(hex.length / 2);
for (let i = 0; i < size; i++) {
buf[offset + i] = parseInt(hex.slice(i * 2, i * 2 + 2).padEnd(2, '0'), 16);
}
}
export function bufReadHex(buf: Uint8Array, start = 0, end?: number) {
return [...buf.slice(start, end)].map(val => val.toString(16).padStart(2, '0')).join('');
}

export class Multiset<T> extends Map<T, number> {
get(key: T) {
return super.get(key) ?? 0;
Expand All @@ -436,5 +451,7 @@ export const Utils = {
shuffle, deepClone, clearRequireCache,
randomElement, forceWrap, splitFirst,
stripHTML, visualize, getString,
escapeRegex, formatSQLArray, Multiset,
escapeRegex, formatSQLArray,
bufFromHex, bufReadHex, bufWriteHex,
Multiset,
};
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"preact-render-to-string": "^5.1.19",
"probe-image-size": "^7.2.3",
"sockjs": "^0.3.21",
"source-map-support": "^0.5.21"
"source-map-support": "^0.5.21",
"ts-chacha20": "^1.2.0"
},
"optionalDependencies": {
"better-sqlite3": "^7.6.2",
Expand Down Expand Up @@ -72,6 +73,7 @@
"@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.5",
"@types/sockjs": "^0.3.33",
"@types/sodium-native": "^2.3.9",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"eslint": "8.5.0",
Expand Down
6 changes: 4 additions & 2 deletions sim/battle-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,12 @@ export class BattleStream extends Streams.ObjectReadWriteStream<string> {
this.battle!.inputLog.push(`>forcelose ${message}`);
break;
case 'reseed':
const seed = message ? message.split(',').map(Number) as PRNGSeed : null;
const seed = message ? message.split(',').map(
n => /[0-9]/.test(n.charAt(0)) ? Number(n) : n
) as PRNGSeed : null;
this.battle!.resetRNG(seed);
// could go inside resetRNG, but this makes using it in `eval` slightly less buggy
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.seed.join(',')}`);
this.battle!.inputLog.push(`>reseed ${this.battle!.prng.getSeed().join(',')}`);
break;
case 'tiebreak':
this.battle!.tiebreak();
Expand Down
8 changes: 4 additions & 4 deletions sim/battle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export class Battle {
(format.playerCount > 2 || this.gameType === 'doubles') ? 2 :
1;
this.prng = options.prng || new PRNG(options.seed || undefined);
this.prngSeed = this.prng.startingSeed.slice() as PRNGSeed;
this.prngSeed = this.prng.startingSeed;
this.rated = options.rated || !!options.rated;
this.reportExactHP = !!format.debug;
this.reportPercentages = false;
Expand Down Expand Up @@ -273,7 +273,7 @@ export class Battle {
this.send = options.send || (() => {});

const inputOptions: {formatid: ID, seed: PRNGSeed, rated?: string | true} = {
formatid: options.formatid, seed: this.prng.seed,
formatid: options.formatid, seed: this.prngSeed,
};
if (this.rated) inputOptions.rated = this.rated;
if (typeof __version !== 'undefined') {
Expand Down Expand Up @@ -340,7 +340,7 @@ export class Battle {
}

random(m?: number, n?: number) {
return this.prng.next(m, n);
return this.prng.random(m, n);
}

randomChance(numerator: number, denominator: number) {
Expand All @@ -353,7 +353,7 @@ export class Battle {
}

/** Note that passing `undefined` resets to the starting seed, but `null` will roll a new seed */
resetRNG(seed: PRNGSeed | null = this.prng.startingSeed) {
resetRNG(seed: PRNGSeed | null = this.prngSeed) {
this.prng = new PRNG(seed);
this.add('message', "The battle's RNG was reset.");
}
Expand Down
2 changes: 1 addition & 1 deletion sim/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export class Pokemon {
set.level = this.battle.clampIntRange(set.adjustLevel || set.level || 100, 1, 9999);
this.level = set.level;
const genders: {[key: string]: GenderName} = {M: 'M', F: 'F', N: 'N'};
this.gender = genders[set.gender] || this.species.gender || (this.battle.random() * 2 < 1 ? 'M' : 'F');
this.gender = genders[set.gender] || this.species.gender || (this.battle.random(2) ? 'F' : 'M');
if (this.gender === 'N') this.gender = '';
this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255;
this.pokeball = this.set.pokeball || 'pokeball';
Expand Down
Loading

0 comments on commit d3e60b3

Please sign in to comment.