From a86d04342d5853c23c7695e03b8c9485a7d62f99 Mon Sep 17 00:00:00 2001 From: larry-the-table-guy <180724454+larry-the-table-guy@users.noreply.github.com> Date: Sat, 9 Nov 2024 06:45:18 -0500 Subject: [PATCH] Data: Fix huge perf bugs in randbat tests, part 1 (#10616) --- data/random-battles/gen3/teams.ts | 4 +- data/random-battles/gen4/teams.ts | 7 ++-- data/random-battles/gen5/teams.ts | 9 +++-- data/random-battles/gen6/teams.ts | 7 ++-- data/random-battles/gen7/teams.ts | 10 +++-- data/random-battles/gen8/teams.ts | 50 ++++++++++++++++++------ data/random-battles/gen9/teams.ts | 56 ++++++++++++++++++++------- data/random-battles/gen9baby/teams.ts | 4 +- sim/teams.ts | 9 +++-- 9 files changed, 105 insertions(+), 51 deletions(-) diff --git a/data/random-battles/gen3/teams.ts b/data/random-battles/gen3/teams.ts index 3570009cf673..511fd1377296 100644 --- a/data/random-battles/gen3/teams.ts +++ b/data/random-battles/gen3/teams.ts @@ -129,9 +129,7 @@ export class RandomGen3Teams extends RandomGen4Teams { // Develop additional move lists const badWithSetup = ['knockoff', 'rapidspin', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen4/teams.ts b/data/random-battles/gen4/teams.ts index 1b84d3bdeae6..1ce2723fe08e 100644 --- a/data/random-battles/gen4/teams.ts +++ b/data/random-battles/gen4/teams.ts @@ -76,6 +76,9 @@ export class RandomGen4Teams extends RandomGen5Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.id === 'metagross'), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status') + .map(move => move.id); } cullMovePool( @@ -164,9 +167,7 @@ export class RandomGen4Teams extends RandomGen5Teams { // Develop additional move lists const badWithSetup = ['pursuit', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen5/teams.ts b/data/random-battles/gen5/teams.ts index 4cc13067b4a2..14636cc136c3 100644 --- a/data/random-battles/gen5/teams.ts +++ b/data/random-battles/gen5/teams.ts @@ -89,6 +89,10 @@ export class RandomGen5Teams extends RandomGen6Teams { ), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + // Nature Power is Earthquake this gen + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status' && move.id !== 'naturepower') + .map(move => move.id); } cullMovePool( @@ -177,10 +181,7 @@ export class RandomGen5Teams extends RandomGen6Teams { // Develop additional move lists const badWithSetup = ['healbell', 'pursuit', 'toxic']; - // Nature Power is Earthquake this gen - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status' && move.id !== 'naturepower') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen6/teams.ts b/data/random-battles/gen6/teams.ts index e54839e0ce74..b989b40c62fe 100644 --- a/data/random-battles/gen6/teams.ts +++ b/data/random-battles/gen6/teams.ts @@ -103,6 +103,9 @@ export class RandomGen6Teams extends RandomGen7Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.baseStats.atk >= 100), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status') + .map(move => move.id); } cullMovePool( @@ -196,9 +199,7 @@ export class RandomGen6Teams extends RandomGen7Teams { // Develop additional move lists const badWithSetup = ['defog', 'dragontail', 'haze', 'healbell', 'nuzzle', 'pursuit', 'rapidspin', 'toxic']; - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen7/teams.ts b/data/random-battles/gen7/teams.ts index fbd939b601a8..3ed12defd32f 100644 --- a/data/random-battles/gen7/teams.ts +++ b/data/random-battles/gen7/teams.ts @@ -99,6 +99,7 @@ function sereneGraceBenefits(move: Move) { export class RandomGen7Teams extends RandomGen8Teams { randomSets: {[species: string]: RandomTeamsTypes.RandomSpeciesData} = require('./sets.json'); + protected cachedStatusMoves: ID[]; constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { super(format, prng); @@ -141,6 +142,10 @@ export class RandomGen7Teams extends RandomGen8Teams { Steel: (movePool, moves, abilities, types, counter, species) => (!counter.get('Steel') && species.baseStats.atk >= 100), Water: (movePool, moves, abilities, types, counter) => !counter.get('Water'), }; + // Nature Power is Tri Attack this gen + this.cachedStatusMoves = this.dex.moves.all() + .filter(move => move.category === 'Status' && move.id !== 'naturepower') + .map(move => move.id); } newQueryMoves( @@ -311,10 +316,7 @@ export class RandomGen7Teams extends RandomGen8Teams { // Develop additional move lists const badWithSetup = ['defog', 'dragontail', 'haze', 'healbell', 'nuzzle', 'pursuit', 'rapidspin', 'toxic']; - // Nature Power is Tri Attack this gen - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status' && move.id !== 'naturepower') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // General incompatibilities const incompatiblePairs = [ diff --git a/data/random-battles/gen8/teams.ts b/data/random-battles/gen8/teams.ts index e8e68b1dc84e..220c94b86964 100644 --- a/data/random-battles/gen8/teams.ts +++ b/data/random-battles/gen8/teams.ts @@ -95,7 +95,7 @@ function sereneGraceBenefits(move: Move) { } export class RandomGen8Teams { - dex: ModdedDex; + readonly dex: ModdedDex; gen: number; factoryTier: string; format: Format; @@ -116,6 +116,11 @@ export class RandomGen8Teams { */ moveEnforcementCheckers: {[k: string]: MoveEnforcementChecker}; + /** Used by .getPools() */ + private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; + private cachedPool: number[] | undefined; + private cachedSpeciesPool: Species[] | undefined; + constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { format = Dex.formats.get(format); this.dex = Dex.forFormat(format); @@ -232,6 +237,9 @@ export class RandomGen8Teams { return abilities.includes('Huge Power') && movePool.includes('aquajet'); }, }; + this.poolsCacheKey = undefined; + this.cachedPool = undefined; + this.cachedSpeciesPool = undefined; } setSeed(prng?: PRNG | PRNGSeed) { @@ -526,19 +534,18 @@ export class RandomGen8Teams { return team; } - randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { - // Picks `n` random pokemon--no repeats, even among formes - // Also need to either normalize for formes or select formes at random - // Unreleased are okay but no CAP - if (requiredType && !this.dex.types.get(requiredType).exists) { - throw new Error(`"${requiredType}" is not a valid type.`); - } - + private getPools(requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Memoize pool and speciesPool because, at least during tests, they are constructed with the same parameters + // hundreds of times and are expensive to compute. const isNotCustom = !ruleTable; - - const pool: number[] = []; + let pool: number[] = []; let speciesPool: Species[] = []; - if (isNotCustom) { + const ck = this.poolsCacheKey; + if (ck && this.cachedPool && this.cachedSpeciesPool && + ck[0] === requiredType && ck[1] === minSourceGen && ck[2] === ruleTable && ck[3] === requireMoves) { + speciesPool = this.cachedSpeciesPool.slice(); + pool = this.cachedPool.slice(); + } else if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; @@ -552,6 +559,9 @@ export class RandomGen8Teams { if (num <= 0 || pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } else { const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; const nonexistentBanReason = ruleTable.check('nonexistent'); @@ -596,7 +606,23 @@ export class RandomGen8Teams { if (pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } + return {pool, speciesPool}; + } + + randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Picks `n` random pokemon--no repeats, even among formes + // Also need to either normalize for formes or select formes at random + // Unreleased are okay but no CAP + if (requiredType && !this.dex.types.get(requiredType).exists) { + throw new Error(`"${requiredType}" is not a valid type.`); + } + + const {pool, speciesPool} = this.getPools(requiredType, minSourceGen, ruleTable, requireMoves); + const isNotCustom = !ruleTable; const hasDexNumber: {[k: string]: number} = {}; for (let i = 0; i < n; i++) { diff --git a/data/random-battles/gen9/teams.ts b/data/random-battles/gen9/teams.ts index cbab9c31e234..b910b80be92d 100644 --- a/data/random-battles/gen9/teams.ts +++ b/data/random-battles/gen9/teams.ts @@ -145,7 +145,7 @@ function sereneGraceBenefits(move: Move) { } export class RandomTeams { - dex: ModdedDex; + readonly dex: ModdedDex; gen: number; factoryTier: string; format: Format; @@ -164,6 +164,12 @@ export class RandomTeams { */ moveEnforcementCheckers: {[k: string]: MoveEnforcementChecker}; + /** Used by .getPools() */ + private poolsCacheKey: [string | undefined, number | undefined, RuleTable | undefined, boolean] | undefined; + private cachedPool: number[] | undefined; + private cachedSpeciesPool: Species[] | undefined; + protected cachedStatusMoves: ID[]; + constructor(format: Format | string, prng: PRNG | PRNGSeed | null) { format = Dex.formats.get(format); this.dex = Dex.forFormat(format); @@ -233,6 +239,10 @@ export class RandomTeams { ), Water: (movePool, moves, abilities, types, counter) => (!counter.get('Water') && !types.includes('Ground')), }; + this.poolsCacheKey = undefined; + this.cachedPool = undefined; + this.cachedSpeciesPool = undefined; + this.cachedStatusMoves = this.dex.moves.all().filter(move => move.category === 'Status').map(move => move.id); } setSeed(prng?: PRNG | PRNGSeed) { @@ -476,9 +486,7 @@ export class RandomTeams { } // Develop additional move lists - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // Team-based move culls if (teamDetails.screens) { @@ -1972,19 +1980,18 @@ export class RandomTeams { return team; } - randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { - // Picks `n` random pokemon--no repeats, even among formes - // Also need to either normalize for formes or select formes at random - // Unreleased are okay but no CAP - if (requiredType && !this.dex.types.get(requiredType).exists) { - throw new Error(`"${requiredType}" is not a valid type.`); - } - + private getPools(requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Memoize pool and speciesPool because, at least during tests, they are constructed with the same parameters + // hundreds of times and are expensive to compute. const isNotCustom = !ruleTable; - - const pool: number[] = []; + let pool: number[] = []; let speciesPool: Species[] = []; - if (isNotCustom) { + const ck = this.poolsCacheKey; + if (ck && this.cachedPool && this.cachedSpeciesPool && + ck[0] === requiredType && ck[1] === minSourceGen && ck[2] === ruleTable && ck[3] === requireMoves) { + speciesPool = this.cachedSpeciesPool.slice(); + pool = this.cachedPool.slice(); + } else if (isNotCustom) { speciesPool = [...this.dex.species.all()]; for (const species of speciesPool) { if (species.isNonstandard && species.isNonstandard !== 'Unobtainable') continue; @@ -1998,6 +2005,9 @@ export class RandomTeams { if (num <= 0 || pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } else { const EXISTENCE_TAG = ['past', 'future', 'lgpe', 'unobtainable', 'cap', 'custom', 'nonexistent']; const nonexistentBanReason = ruleTable.check('nonexistent'); @@ -2042,7 +2052,23 @@ export class RandomTeams { if (pool.includes(num)) continue; pool.push(num); } + this.poolsCacheKey = [requiredType, minSourceGen, ruleTable, requireMoves]; + this.cachedPool = pool.slice(); + this.cachedSpeciesPool = speciesPool.slice(); } + return {pool, speciesPool}; + } + + randomNPokemon(n: number, requiredType?: string, minSourceGen?: number, ruleTable?: RuleTable, requireMoves = false) { + // Picks `n` random pokemon--no repeats, even among formes + // Also need to either normalize for formes or select formes at random + // Unreleased are okay but no CAP + if (requiredType && !this.dex.types.get(requiredType).exists) { + throw new Error(`"${requiredType}" is not a valid type.`); + } + + const {pool, speciesPool} = this.getPools(requiredType, minSourceGen, ruleTable, requireMoves); + const isNotCustom = !ruleTable; const hasDexNumber: {[k: string]: number} = {}; for (let i = 0; i < n; i++) { diff --git a/data/random-battles/gen9baby/teams.ts b/data/random-battles/gen9baby/teams.ts index 18836784c297..da9977951a17 100644 --- a/data/random-battles/gen9baby/teams.ts +++ b/data/random-battles/gen9baby/teams.ts @@ -102,9 +102,7 @@ export class RandomBabyTeams extends RandomTeams { } // Create list of all status moves to be used later - const statusMoves = this.dex.moves.all() - .filter(move => move.category === 'Status') - .map(move => move.id); + const statusMoves = this.cachedStatusMoves; // Team-based move culls if (teamDetails.screens && movePool.length >= this.maxMoveCount + 2) { diff --git a/sim/teams.ts b/sim/teams.ts index 1c0b8d91e405..67536e1ade3f 100644 --- a/sim/teams.ts +++ b/sim/teams.ts @@ -618,13 +618,14 @@ export const Teams = new class Teams { getGenerator(format: Format | string, seed: PRNG | PRNGSeed | null = null) { let TeamGenerator; format = Dex.formats.get(format); - if (toID(format).includes('gen9computergeneratedteams')) { + const formatID = toID(format); + if (formatID.includes('gen9computergeneratedteams')) { TeamGenerator = require(Dex.forFormat(format).dataDir + '/cg-teams').default; - } else if (toID(format).includes('gen9superstaffbrosultimate')) { + } else if (formatID.includes('gen9superstaffbrosultimate')) { TeamGenerator = require(`../data/mods/gen9ssb/random-teams`).default; - } else if (toID(format).includes('gen9babyrandombattle')) { + } else if (formatID.includes('gen9babyrandombattle')) { TeamGenerator = require(`../data/random-battles/gen9baby/teams`).default; - } else if (toID(format).includes('gen9randombattle') && format.ruleTable?.has('+pokemontag:cap')) { + } else if (formatID.includes('gen9randombattle') && format.ruleTable?.has('+pokemontag:cap')) { TeamGenerator = require(`../data/random-battles/gen9cap/teams`).default; } else { TeamGenerator = require(`../data/random-battles/${format.mod}/teams`).default;