Files
pokemon-showdown/test/random-battles/tools.js
Guangcong Luo 78439b4a02 Update to ESLint 9 (#10926)
ESLint has a whole new config format, so I figure it's a good time to
make the config system saner.

- First, we no longer have separate eslint-no-types configs. Lint
  performance shouldn't be enough of a problem to justify the
  relevant maintenance complexity.

- Second, our base config should work out-of-the-box now. `npx eslint`
  will work as expected, without any CLI flags. You should still use
  `npm run lint` which adds the `--cached` flag for performance.

- Third, whatever updates I did fixed style linting, which apparently
  has been bugged for quite some time, considering all the obvious
  mixed-tabs-and-spaces issues I found in the upgrade.

Also here are some changes to our style rules. In particular:

- Curly brackets (for objects etc) now have spaces inside them. Sorry
  for the huge change. ESLint doesn't support our old style, and most
  projects use Prettier style, so we might as well match them in this way.
  See https://github.com/eslint-stylistic/eslint-stylistic/issues/415

- String + number concatenation is no longer allowed. We now
  consistently use template strings for this.
2025-02-25 20:03:46 -08:00

196 lines
7.1 KiB
JavaScript

/**
* Tools for testing Random Battles.
*
* @author Annika
* Some helper functions by Slayer95 have also been added.
*/
'use strict';
const assert = require("../assert");
const Teams = require('./../../dist/sim/teams').Teams;
const { TeamValidator, PokemonSources } = require('../../dist/sim/team-validator');
/**
* Unit test helper for Pokemon sets
*
* @param {ID} pokemon the ID of the Pokemon whose set is to be tested
* @param {{format?: string, rounds?: number, isDoubles?: boolean, isLead?: boolean, isDynamax?: boolean, seed?: PRNGSeed}} options
* @param {(set: RandomTeamsTypes.RandomSet) => void} test a function called on each set
*/
function testSet(pokemon, options, test) {
const rounds = options.rounds || 1000;
const isDoubles = options.isDoubles || (options.format && options.format.includes('doubles'));
const isDynamax = options.isDynamax || !(options.format && options.format.includes('nodmax'));
const generator = Teams.getGenerator(options.format, [0, 0, 0, 0]);
for (let i = 0; i < rounds; i++) {
// If undefined, test lead 1/6 of the time
const isLead = options.isLead === undefined ? i % 6 === 2 : options.isLead;
generator.setSeed(options.seed || [i, i, i, i]);
const set = generator.randomSet(pokemon, {}, isLead, isDoubles, isDynamax);
test(set);
}
}
/**
* Tests that a Pokémon always gets STAB moves.
*
* @param {ID} pokemon
* @param {{format?: string, rounds?: number, isDoubles?: boolean, isLead?: boolean, isDynamax?: boolean, seed?: PRNGSeed}} options
* @param {string[] | undefined} types
*/
function testHasSTAB(pokemon, options, types = undefined) {
const dex = Dex.forFormat(options.format || 'gen8randombattle');
types = types || dex.species.get(pokemon).types;
testSet(pokemon, options, set => {
assert(
set.moves.some(move => types.includes(dex.moves.get(move).type)),
`${pokemon} should have at least one STAB move (generated moveset: ${set.moves})`
);
});
}
/**
* Tests that a Pokémon does not get two moves together.
*
* @param {ID} pokemon the ID of the Pokemon whose set is to be tested
* @param {{format?: string, rounds?: number, isDoubles?: boolean, isLead?: boolean, isDynamax?: boolean, seed?: PRNGSeed}} options
* @param {ID} move1
* @param {ID} move2
*/
function testNotBothMoves(pokemon, options, move1, move2) {
testSet(pokemon, options, set => {
assert(
!(set.moves.includes(move1) && set.moves.includes(move2)),
`${pokemon} should not generate both "${move1}" and "${move2}" (generated moveset: ${set.moves})`
);
});
}
/**
* Tests that a Pokémon does not get two copies of Hidden Power.
*
* @param {ID} pokemon the ID of the Pokemon whose set is to be tested
* @param {{format?: string, rounds?: number, isDoubles?: boolean, isLead?: boolean, isDynamax?: boolean, seed?: PRNGSeed}} options
*/
function testHiddenPower(pokemon, options) {
testSet(pokemon, options, set => {
assert.equal(set.moves.length, 4, `fewer than 4 moves (got ${JSON.stringify(set.moves)})`);
assert(
set.moves.filter(m => m.startsWith('hiddenpower')).length < 2,
`multiple Hidden Power moves (got ${JSON.stringify(set.moves)})`
);
});
}
/**
* Tests that a Pokémon always gets a move.
*
* @param {ID} pokemon the ID of the Pokemon whose set is to be tested
* @param {{format?: string, rounds?: number, isDoubles?: boolean, isLead?: boolean, isDynamax?: boolean, seed?: PRNGSeed}} options
* @param {ID} move
*/
function testAlwaysHasMove(pokemon, options, move) {
testSet(pokemon, options, set => {
assert(
set.moves.includes(move),
`${pokemon} should always generate "${move}" (generated moveset: ${set.moves})`
);
});
}
/**
* Unit test helper for Pokemon teams
*
* @param {{format?: string, rounds?: number, seed?: PRNGSeed}} options
* @param {(team: RandomTeamsTypes.RandomSet[]) => void} test a function called on each team
*/
function testTeam(options, test) {
const rounds = options.rounds || 1000;
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].join(','));
const team = generator.getTeam();
test(team);
}
}
/**
* Checks if a set is valid.
*
* @param {Format} format the format that the team is being validated in
* @param {RandomTeamsTypes.RandomSet} set
*/
function assertSetValidity(format, set) {
const dex = Dex.forFormat(format);
const species = dex.species.get(set.species || set.name);
const setString = JSON.stringify(set);
// According to Random Battles room staff, we should not ensure that HP IVs are valid for
// BSS formats. This is because level 100 Pokémon can be hypertrained
// and then automatically downleveled to level 100 for the Battle Spot.
if (!format.id.includes('bss')) {
const valid = (new TeamValidator(format))
.validateStats(set, species, new PokemonSources())
// Suppress errors about mistaken EV quantities
.filter(f => !f.includes(' EVs'));
assert.equal(valid.length, 0, `Invalid stats: ${valid} (set: ${setString})`);
}
// We check `dex.gen` here because Format#gen is 0 in the current gen, while ModdedDex#gen is never 0.
assert(species.exists, `The species "${species.name}" does not exist. (set: ${setString})`);
assert(species.gen <= dex.gen, `The species "${species.name}" is from a newer generation. (set: ${setString})`);
if (set.item) {
const item = dex.items.get(set.item);
assert(item.exists, `The item "${item.name}" does not exist. (set: ${setString})`);
assert(item.gen <= dex.gen, `The item "${item.name}" is from a newer generation. (set: ${setString})`);
}
if (set.ability && set.ability !== 'None') {
const ability = dex.abilities.get(set.ability);
assert(ability.exists, `The ability "${ability.name}" does not exist. (set: ${setString})`);
assert(ability.gen <= dex.gen, `The ability "${ability.name}" is from a newer generation. (set: ${setString})`);
} else {
assert(dex.gen < 3, `This set does not have an ability, but is intended for use in Gen 3 or later. (set: ${setString})`);
}
// Arceus plate check
if (
species.baseSpecies === 'Arceus' &&
species.types[0] !== 'Normal' &&
(dex.gen !== 7 || !set.item.endsWith(' Z')) &&
format.team !== 'randomHC'
) {
assert(set.item.endsWith(' Plate'), `${species.name} doesn't have a Plate (got "${set.item}" instead)`);
}
assert(set.moves.filter(m => m.startsWith('hiddenpower')).length <= 1, `This set has multiple Hidden Power moves. (set: ${setString})`);
}
/**
* Checks if a move is valid on a set.
*
* @param {ID} move
* @param {RandomTeamsTypes.RandomSet} set
* @param {ID} tier
* @param {ID} mod
* @returns {boolean}
*/
function validateLearnset(move, set, tier, mod = 'gen8') {
const validator = TeamValidator.get(`${mod}${tier}`);
const species = validator.dex.species.get(set.species || set.name);
return !validator.checkCanLearn(move, species);
}
exports.testSet = testSet;
exports.testAlwaysHasMove = testAlwaysHasMove;
exports.testNotBothMoves = testNotBothMoves;
exports.testHiddenPower = testHiddenPower;
exports.testTeam = testTeam;
exports.testHasSTAB = testHasSTAB;
exports.assertSetValidity = assertSetValidity;
exports.validateLearnset = validateLearnset;