/** * Miscellaneous Random Battle-related tests not associated with a particular generation. */ 'use strict'; const assert = require('../assert'); const common = require('../common'); const { Utils } = require('../../dist/lib'); const { testTeam, assertSetValidity, validateLearnset } = require('./tools'); const { default: Dex } = require('../../dist/sim/dex'); describe('value rule support (slow)', () => { it('should generate teams of the proper length for the format (i.e. support Max Team Size)', () => { testTeam({ format: 'gen9randombattle', rounds: 100 }, team => assert.equal(team.length, 6)); testTeam({ format: 'gen9challengecup1v1', rounds: 100 }, team => assert.equal(team.length, 6)); testTeam({ format: 'gen9hackmonscup', rounds: 100 }, team => assert.equal(team.length, 6)); testTeam({ format: 'gen8multirandombattle', rounds: 100 }, team => assert.equal(team.length, 3)); testTeam({ format: 'gen8cap1v1', rounds: 100 }, team => assert.equal(team.length, 3)); testTeam({ format: 'gen7randombattle', rounds: 100 }, team => assert.equal(team.length, 6)); }); for (let gen = 1; gen <= 9; gen++) { const formatID = `gen${gen}randombattle`; const dex = Dex.forFormat(formatID); for (const count of [1, 3, 24]) { // This is tough to test in Gen 1 because we don't know how many moves a Pokémon ought to have, // so we only test 'Max Move Count = 1' in Gen 1. if (gen === 1 && count !== 1) continue; const format = Dex.formats.get(`${formatID}@@@Max Move Count = ${count}`); // New set format if ([2, 3, 4, 5, 6, 7, 9].includes(gen)) { // Due to frontloading of moveset generation, formats with the new set format do not support // Max Move Counts less than 4 if (count < 4) continue; const setsJSON = require(`../../dist/data/random-battles/gen${gen}/sets.json`); it(`${format.name} should support Max Move Count = ${count}`, () => { testTeam({ format, rounds: 50 }, team => { for (const set of team) { let species = set.species; // Formes make this test code really complicated, so we skip them // (This is because info about formes isn't passed through) if (dex.species.get(species).otherFormes?.length || dex.species.get(species).forme) continue; if (set.gigantamax && !set.species.endsWith('max')) species += '-Gmax'; // This is an array because the new set format can have multiple sets, and hence multiple possible // set lengths. const expectedMoves = []; for (const s of setsJSON[dex.species.get(species).id].sets) { let seenHP = false; let totalMoves = 0; for (const move of s.movepool) { if (move.startsWith('hiddenpower')) { if (seenHP) continue; seenHP = true; } totalMoves++; } expectedMoves.push(Math.min(totalMoves, count)); } assert(expectedMoves.includes(set.moves.length), `${species} should have ${expectedMoves.toString()} moves (moves=${set.moves})`); } }); }); } else { const dataJSON = require(`../../dist/data/random-battles/gen${gen}/data.json`); it(`${format.name} should support Max Move Count = ${count}`, () => { testTeam({ format, rounds: 50 }, team => { for (const set of team) { let species = set.species; // Formes make this test code really complicated, so we skip them // (This is because info about formes isn't passed through) if (dex.species.get(species).otherFormes?.length || dex.species.get(species).forme) continue; if (set.gigantamax && !set.species.endsWith('max')) species += '-Gmax'; // If the Pokémon has less than the max in its movepool, we should // just see all those moves. let totalMoves = 0; let seenHP = false; for (const move of dataJSON[dex.species.get(species).id].moves) { if (move.startsWith('hiddenpower')) { if (seenHP) continue; seenHP = true; } totalMoves++; } const expected = Math.min(totalMoves, count); assert.equal(set.moves.length, expected, `${species} should have ${expected} moves (moves=${set.moves})`); } }); }); } } } for (const format of Dex.formats.all()) { if (!format.team) continue; if (Dex.formats.getRuleTable(format).has('adjustleveldown') || Dex.formats.getRuleTable(format).has('adjustlevel')) continue; // already adjusts level for (const level of [1, 99999]) { it(`${format.name} should support Adjust Level = ${level}`, () => { testTeam({ format: `${format.id}@@@Adjust Level = ${level}`, rounds: 50 }, team => { for (const set of team) { assert.equal(set.level, level); } }); }); } } }); describe("New set format (slow)", () => { // formatInfo lists filenames and roles for each format const formatInfo = { "gen2randombattle": { filename: "gen2/sets", roles: ["Fast Attacker", "Setup Sweeper", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Generalist", "Thief user"], }, "gen3randombattle": { filename: "gen3/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Generalist", "Berry Sweeper"], }, "gen4randombattle": { filename: "gen4/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"], }, "gen5randombattle": { filename: "gen5/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "Spinner"], }, "gen6randombattle": { filename: "gen6/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"], }, "gen7randombattle": { filename: "gen7/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Z-Move user", "Bulky Attacker", "Bulky Setup", "Staller", "Bulky Support", "Fast Support", "AV Pivot"], }, "gen9randombattle": { filename: "gen9/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"], }, "gen9randomdoublesbattle": { filename: "gen9/doubles-sets", roles: ["Doubles Fast Attacker", "Doubles Setup Sweeper", "Doubles Wallbreaker", "Tera Blast user", "Doubles Bulky Attacker", "Doubles Bulky Setup", "Offensive Protect", "Bulky Protect", "Doubles Support", "Choice Item user"], }, "gen9babyrandombattle": { filename: "gen9baby/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Bulky Support", "Fast Support"], }, "gen9randombattle@@@+cap": { filename: "gen9cap/sets", roles: ["Fast Attacker", "Setup Sweeper", "Wallbreaker", "Tera Blast user", "Bulky Attacker", "Bulky Setup", "Fast Bulky Setup", "Bulky Support", "Fast Support", "AV Pivot"], }, }; for (const format of Object.keys(formatInfo)) { const filename = formatInfo[format].filename; const setsJSON = require(`../../dist/data/random-battles/${filename}.json`); const dex = common.mod(common.getFormat({ formatid: format }).mod).dex; // verifies format exists const genNum = dex.gen; const rounds = 100; it(`${filename}.json should have valid set data`, () => { const validRoles = formatInfo[format].roles; for (const [id, sets] of Object.entries(setsJSON)) { const species = dex.species.get(id); assert(species.exists, `In ${format}, misspelled species ID: ${id}`); assert(Array.isArray(sets.sets)); for (const set of sets.sets) { assert(validRoles.includes(set.role), `In ${format}, set for ${species.name} has invalid role: ${set.role}`); for (const move of set.movepool) { const dexMove = dex.moves.get(move); assert(dexMove.exists, `In ${format}, ${species.name} has invalid move: ${move}`); // Old gens have moves in id form, currently. if (genNum === 9) { assert.equal(move, dexMove.name, `In ${format}, ${species.name} has misformatted move: ${move}`); } else { assert(move === dexMove.id || move.startsWith('hiddenpower'), `In ${format}, ${species.name} has misformatted move: ${move}`); } assert(validateLearnset(dexMove, { species }, 'ubers', `gen${genNum}`), `In ${format}, ${species.name} can't learn ${move}`); } for (let i = 0; i < set.movepool.length - 1; i++) { assert(set.movepool[i + 1] > set.movepool[i], `In ${format}, ${species.name} movepool should be sorted alphabetically`); } if (genNum >= 3) { assert(set.abilities, `In ${format}, ${set.abilities} has no abilities`); for (const ability of set.abilities) { const dexAbility = dex.abilities.get(ability); assert(dexAbility.exists, `In ${format}, ${species.name} has invalid ability: ${ability}`); // Mega/Primal Pokemon have abilities from their base formes const allowedAbilities = new Set(Object.values((species.battleOnly && !species.requiredAbility) ? dex.species.get(species.battleOnly).abilities : species.abilities)); if (species.unreleasedHidden) allowedAbilities.delete(species.abilities.H); assert(allowedAbilities.has(ability), `In ${format}, ${species.name} can't have ${ability}`); } for (let i = 0; i < set.abilities.length - 1; i++) { assert(set.abilities[i + 1] > set.abilities[i], `In ${format}, ${species.name} abilities should be sorted alphabetically`); } } if (genNum === 9) { assert(set.teraTypes, `In ${format}, ${species.name} has no Tera Types`); for (const type of set.teraTypes) { const dexType = dex.types.get(type); assert(dexType.exists, `In ${format}, ${species.name} has invalid Tera Type: ${type}`); assert.equal(type, dexType.name, `In ${format}, ${species.name} has misformatted Tera Type: ${type}`); } for (let i = 0; i < set.teraTypes.length - 1; i++) { assert(set.teraTypes[i + 1] > set.teraTypes[i], `In ${format}, ${species.name} teraTypes should be sorted alphabetically`); } } if (set.preferredTypes) { for (const type of set.preferredTypes) { const dexType = dex.types.get(type); assert(dexType.exists, `In ${format}, ${species.name} has invalid Preferred Type: ${type}`); assert.equal(type, dexType.name, `In ${format}, ${species.name} has misformatted Preferred Type: ${type}`); } for (let i = 0; i < set.preferredTypes.length - 1; i++) { assert(set.preferredTypes[i + 1] > set.preferredTypes[i], `In ${format}, ${species.name} preferredTypes should be sorted alphabetically`); } } } } }); it('all Pokemon should have 4 moves, except for Ditto and Unown', () => { testTeam({ format, rounds }, team => { for (const pokemon of team) assert(pokemon.name === 'Ditto' || pokemon.name === 'Unown' || pokemon.moves.length === 4, `In ${format}, ${pokemon.name} can generate with ${pokemon.moves.length} moves`); }); }); it('all moves on all sets should exist and be obtainable', () => { const generator = Teams.getGenerator(format); for (const pokemon of Object.keys(setsJSON)) { const species = dex.species.get(pokemon); assert(species.exists, `In ${format}, Pokemon ${species} does not exist`); const sets = setsJSON[pokemon]["sets"]; const types = species.types; for (const set of sets) { assert(set.movepool.every(m => dex.moves.get(m).exists), `In ${format}, for Pokemon ${species}, one of ${set.movepool} does not exist.`); const role = set.role; const moves = new Set(set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id))); const abilities = set.abilities || []; const specialTypes = genNum === 9 ? set.teraTypes : set.preferredTypes; // Go through all possible teamDetails combinations, if necessary for (let j = 0; j < rounds; j++) { // In Gens 2-3, if a set has multiple preferred types, we enforce moves of all the types. const specialType = specialTypes ? (genNum > 3 ? specialTypes[j % specialTypes.length] : specialTypes.join()) : ''; // Generate a moveset for each combination of relevant teamDetails. Spikes is relevant for Gen 2. for (let i = 0; i < 16; i++) { const rapidSpin = i % 2; const stealthRock = Math.floor(i / 2) % 2; const stickyWeb = Math.floor(i / 4) % 2; const spikes = Math.floor(i / 8) % 2; const screens = Math.floor(i / 2) % 2; const teamDetails = { rapidSpin, stealthRock, stickyWeb, spikes, screens }; // randomMoveset() deletes moves from the movepool, so recreate it every time const movePool = set.movepool.map(m => (m.startsWith('hiddenpower') ? m : dex.moves.get(m).id)); let moveSet; if (genNum === 9) { moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, format.includes('doubles'), movePool, specialType, role); } else { moveSet = generator.randomMoveset(types, abilities, teamDetails, species, false, movePool, specialType, role); } for (const move of moveSet) moves.delete(move); if (!moves.size) break; } if (!moves.size) break; } assert.false(moves.size, `In ${format}, the following moves on ${species.name} are unused: ${[...moves].join(', ')}`); } } }); } }); describe('randomly generated teams should be valid (slow)', () => { for (const format of Dex.formats.all()) { if (!format.team) continue; // format doesn't use randomly generated teams if (format.mod === 'gen9ssb') continue; // Temporary it(`should generate valid ${format} teams`, function () { this.timeout(0); const targetTeamSize = Dex.formats.getRuleTable(format).maxTeamSize; testTeam({ format: format.id }, team => { assert.equal(team.length, targetTeamSize, `Team of incorrect size (should have ${targetTeamSize} Pokémon but actually has ${team.length} Pokémon): ${JSON.stringify(team)}`); for (const set of team) { assertSetValidity(format, set); } }); }); } }); describe('Battle Factory and BSS Factory data should be valid (slow)', () => { for (const filename of ['gen9/factory-sets', 'gen8/factory-sets', 'gen8/bss-factory-sets', 'gen7/bss-factory-sets', 'gen7/factory-sets', 'gen6/factory-sets']) { it(`${filename}.json should contain valid sets`, function () { this.timeout(0); const setsJSON = require(`../../dist/data/random-battles/${filename}.json`); const mod = filename.split('/')[0] || 'gen' + Dex.gen; const genNum = isNaN(mod[3]) ? Dex.gen : parseInt(mod[3]); const dex = Dex.mod(mod); for (const type in setsJSON) { const typeTable = filename.includes('bss-factory-sets') ? setsJSON : setsJSON[type]; let vType; if (filename.includes('bss-factory-sets')) { vType = `battle${genNum === 8 ? 'stadium' : 'spot'}singles`; } else if (type === 'Mono') { vType = 'monotype'; } else if (type === 'Uber') { vType = 'ubers'; } else { vType = type.toLowerCase(); } for (const species in typeTable) { const speciesData = typeTable[species]; for (const set of speciesData.sets) { const species = dex.species.get(set.species); assert(species.exists, `invalid species "${set.species}" of ${species}`); assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${species}`); assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${species}`); for (const itemName of [].concat(set.item)) { if (!itemName) continue; const item = dex.items.get(itemName); assert(item.exists, `invalid item "${itemName}" of ${species}`); assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${species}`); } for (const abilityName of [].concat(set.ability)) { const ability = dex.abilities.get(abilityName); assert(ability.exists, `invalid ability "${abilityName}" of ${species}`); assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${species}`); const allowedAbilities = new Set(Object.values((species.battleOnly && !species.requiredAbility) ? dex.species.get(species.battleOnly).abilities : species.abilities)); if (species.unreleasedHidden) allowedAbilities.delete(species.abilities.H); assert(allowedAbilities.has(abilityName), `${species.name} can't have ${abilityName}`); } for (const natureName of [].concat(set.nature)) { const nature = dex.natures.get(natureName); assert(nature.exists, `invalid nature "${natureName}" of ${species}`); assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${species}`); } for (const moveSpec of set.moves) { for (const moveName of [].concat(moveSpec)) { const move = dex.moves.get(moveName); assert(move.exists, `invalid move "${moveName}" of ${species}`); assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${species}`); if (species.id === 'zacian' && [].concat(set.item).includes('Rusted Sword') && moveName === 'Behemoth Blade') continue; assert(validateLearnset(move, set, vType, mod), `illegal move "${moveName}" of ${species}`); } } // Check that no moves appear more than once in a set assert.equal(set.moves.flat(1).length, new Set(set.moves.flat(1)).size, `${species} has repeat moves`); assert(!!set.evs, `Set of ${species} has no EVs specified`); const keys = Object.keys(set.evs); let totalEVs = 0; for (const ev of keys) { assert(dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${species}`); totalEVs += set.evs[ev]; assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${species}`); } const sortedKeys = Utils.sortBy([...keys], ev => dex.stats.ids().indexOf(ev)); assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${species}, possibly because one of them is for the wrong stat`); assert(totalEVs <= 510, `more than 510 EVs on set of ${species}`); if (genNum === 9) { assert(set.teraType, `missing Tera Types on set of ${species}`); for (const type of set.teraType) { const dexType = dex.types.get(type); assert(dexType.exists, `${species} has invalid Tera Type: ${type}`); assert.equal(type, dexType.name, `${species.name} has misformatted Tera Type: ${type}`); } } } if (filename === 'gen9/factory-sets') { // Set weights should add up to 100 let totalWeight = 0; for (const set of speciesData.sets) { totalWeight += set.weight; } assert.equal(totalWeight, 100, `Total set weight for ${species} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`); } } } }); } }); describe('[Gen 9] BSS Factory data should be valid (slow)', () => { it(`gen9/bss-factory-sets.json should contain valid sets`, function () { this.timeout(0); const setsJSON = require(`../../dist/data/random-battles/gen9/bss-factory-sets.json`); const mod = 'gen9'; const genNum = 9; for (const speciesid in setsJSON) { const vType = 'bssregh'; let totalWeight = 0; for (const set of setsJSON[speciesid].sets) { totalWeight += set.weight; const species = Dex.species.get(set.species); assert(species.exists, `invalid species "${set.species}" of ${speciesid}`); assert(!species.isNonstandard, `illegal species "${set.species}" of ${speciesid}`); assert.equal(species.name, set.species, `miscapitalized species "${set.species}" of ${speciesid}`); assert(species.id.startsWith(toID(species.baseSpecies)), `non-matching species "${set.species}" of ${speciesid}`); assert(!species.battleOnly, `invalid battle-only forme "${set.species}" of ${speciesid}`); for (const itemName of [].concat(set.item)) { if (!itemName && [].concat(...set.moves).includes("Acrobatics")) continue; const item = Dex.forGen(genNum).items.get(itemName); assert(item.exists, `invalid item "${itemName}" of ${speciesid}`); assert.equal(item.name, itemName, `miscapitalized item "${itemName}" of ${speciesid}`); } for (const abilityName of [].concat(set.ability)) { const ability = Dex.forGen(genNum).abilities.get(abilityName); assert(ability.exists, `invalid ability "${abilityName}" of ${speciesid}`); assert.equal(ability.name, abilityName, `miscapitalized ability "${abilityName}" of ${speciesid}`); } for (const natureName of [].concat(set.nature)) { const nature = Dex.forGen(genNum).natures.get(natureName); assert(nature.exists, `invalid nature "${natureName}" of ${speciesid}`); assert.equal(nature.name, natureName, `miscapitalized nature "${natureName}" of ${speciesid}`); } for (const moveSpec of set.moves) { for (const moveName of [].concat(moveSpec)) { const move = Dex.forGen(genNum).moves.get(moveName); assert(move.exists, `invalid move "${moveName}" of ${speciesid}`); assert.equal(move.name, moveName, `miscapitalized move "${moveName}" ≠ "${move.name}" of ${speciesid}`); assert(validateLearnset(move, set, vType, mod), `illegal move "${moveName}" of ${speciesid}`); } } assert(!!set.evs, `Set of ${speciesid} has no EVs specified`); const keys = Object.keys(set.evs); let totalEVs = 0; for (const ev of keys) { assert(Dex.stats.ids().includes(ev), `Invalid EV key (${ev}) on set of ${speciesid}`); totalEVs += set.evs[ev]; assert.equal(set.evs[ev] % 4, 0, `EVs of ${ev} not divisible by 4 on ${speciesid}`); } const sortedKeys = Utils.sortBy([...keys], ev => Dex.stats.ids().indexOf(ev)); assert.deepEqual(keys, sortedKeys, `EVs out of order on set of ${speciesid}, possibly because one of them is for the wrong stat`); assert(totalEVs <= 510, `more than 510 EVs on set of ${speciesid}`); } // Some species have 1/3 probability for each set if (totalWeight === 99) totalWeight += 1; assert.equal(totalWeight, 100, `Total set weight for ${speciesid} is ${totalWeight < 100 ? 'less' : 'greater'} than 100%`); } }); });