pokemon-showdown/data/random-battles/gen9/teams.ts
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

3013 lines
108 KiB
TypeScript

import { Dex, toID } from '../../../sim/dex';
import { Utils } from '../../../lib';
import { PRNG, type PRNGSeed } from '../../../sim/prng';
import { type RuleTable } from '../../../sim/dex-formats';
import { Tags } from './../../tags';
import { Teams } from '../../../sim/teams';
export interface TeamData {
typeCount: { [k: string]: number };
typeComboCount: { [k: string]: number };
baseFormes: { [k: string]: number };
megaCount?: number;
zCount?: number;
wantsTeraCount?: number;
has: { [k: string]: number };
forceResult: boolean;
weaknesses: { [k: string]: number };
resistances: { [k: string]: number };
weather?: string;
eeveeLimCount?: number;
gigantamax?: boolean;
}
export interface BattleFactorySpecies {
sets: BattleFactorySet[];
weight: number;
}
interface BattleFactorySet {
species: string;
weight: number;
item: string[];
ability: string[];
nature: string[];
moves: string[][];
teraType: string[];
gender?: string;
wantsTera?: boolean;
evs?: Partial<StatsTable>;
ivs?: Partial<StatsTable>;
shiny?: boolean;
}
interface BSSFactorySet {
species: string;
weight: number;
item: string[];
ability: string;
nature: string;
moves: string[][];
teraType: string[];
gender?: string;
wantsTera?: boolean;
evs: number[];
ivs?: number[];
}
export class MoveCounter extends Utils.Multiset<string> {
damagingMoves: Set<Move>;
constructor() {
super();
this.damagingMoves = new Set();
}
}
type MoveEnforcementChecker = (
movePool: string[], moves: Set<string>, abilities: string[], types: string[],
counter: MoveCounter, species: Species, teamDetails: RandomTeamsTypes.TeamDetails,
isLead: boolean, isDoubles: boolean, teraType: string, role: RandomTeamsTypes.Role,
) => boolean;
// Moves that restore HP:
const RECOVERY_MOVES = [
'healorder', 'milkdrink', 'moonlight', 'morningsun', 'recover', 'roost', 'shoreup', 'slackoff', 'softboiled', 'strengthsap', 'synthesis',
];
// Moves that drop stats:
const CONTRARY_MOVES = [
'armorcannon', 'closecombat', 'leafstorm', 'makeitrain', 'overheat', 'spinout', 'superpower', 'vcreate',
];
// Moves that boost Attack:
const PHYSICAL_SETUP = [
'bellydrum', 'bulkup', 'coil', 'curse', 'dragondance', 'honeclaws', 'howl', 'meditate', 'poweruppunch', 'swordsdance', 'tidyup', 'victorydance',
];
// Moves which boost Special Attack:
const SPECIAL_SETUP = [
'calmmind', 'chargebeam', 'geomancy', 'nastyplot', 'quiverdance', 'tailglow', 'takeheart', 'torchsong',
];
// Moves that boost Attack AND Special Attack:
const MIXED_SETUP = [
'clangoroussoul', 'growth', 'happyhour', 'holdhands', 'noretreat', 'shellsmash', 'workup',
];
// Some moves that only boost Speed:
const SPEED_SETUP = [
'agility', 'autotomize', 'flamecharge', 'rockpolish', 'snowscape', 'trailblaze',
];
// Conglomerate for ease of access
const SETUP = [
'acidarmor', 'agility', 'autotomize', 'bellydrum', 'bulkup', 'calmmind', 'clangoroussoul', 'coil', 'cosmicpower', 'curse', 'dragondance',
'flamecharge', 'growth', 'honeclaws', 'howl', 'irondefense', 'meditate', 'nastyplot', 'noretreat', 'poweruppunch', 'quiverdance',
'rockpolish', 'shellsmash', 'shiftgear', 'swordsdance', 'tailglow', 'takeheart', 'tidyup', 'trailblaze', 'workup', 'victorydance',
];
const SPEED_CONTROL = [
'electroweb', 'glare', 'icywind', 'lowsweep', 'nuzzle', 'quash', 'tailwind', 'thunderwave', 'trickroom',
];
// Moves that shouldn't be the only STAB moves:
const NO_STAB = [
'accelerock', 'aquajet', 'bounce', 'breakingswipe', 'bulletpunch', 'chatter', 'chloroblast', 'circlethrow', 'clearsmog', 'covet',
'dragontail', 'doomdesire', 'electroweb', 'eruption', 'explosion', 'fakeout', 'feint', 'flamecharge', 'flipturn', 'futuresight',
'grassyglide', 'iceshard', 'icywind', 'incinerate', 'infestation', 'machpunch', 'meteorbeam', 'mortalspin', 'nuzzle', 'pluck', 'pursuit',
'quickattack', 'rapidspin', 'reversal', 'selfdestruct', 'shadowsneak', 'skydrop', 'snarl', 'strugglebug', 'suckerpunch', 'uturn',
'vacuumwave', 'voltswitch', 'watershuriken', 'waterspout',
];
// Hazard-setting moves
const HAZARDS = [
'spikes', 'stealthrock', 'stickyweb', 'toxicspikes',
];
// Protect and its variants
const PROTECT_MOVES = [
'banefulbunker', 'burningbulwark', 'protect', 'silktrap', 'spikyshield',
];
// Moves that switch the user out
const PIVOT_MOVES = [
'chillyreception', 'flipturn', 'partingshot', 'shedtail', 'teleport', 'uturn', 'voltswitch',
];
// Moves that should be paired together when possible
const MOVE_PAIRS = [
['lightscreen', 'reflect'],
['sleeptalk', 'rest'],
['protect', 'wish'],
['leechseed', 'protect'],
['leechseed', 'substitute'],
];
/** Pokemon who always want priority STAB, and are fine with it as its only STAB move of that type */
const PRIORITY_POKEMON = [
'breloom', 'brutebonnet', 'cacturne', 'honchkrow', 'mimikyu', 'ragingbolt', 'scizor',
];
/** Pokemon who should never be in the lead slot */
const NO_LEAD_POKEMON = [
'Zacian', 'Zamazenta',
];
const DOUBLES_NO_LEAD_POKEMON = [
'Basculegion', 'Houndstone', 'Iron Bundle', 'Roaring Moon', 'Zacian', 'Zamazenta',
];
const DEFENSIVE_TERA_BLAST_USERS = [
'alcremie', 'bellossom', 'comfey', 'fezandipiti', 'florges', 'raikou',
];
function sereneGraceBenefits(move: Move) {
return move.secondary?.chance && move.secondary.chance > 20 && move.secondary.chance < 100;
}
export class RandomTeams {
readonly dex: ModdedDex;
gen: number;
factoryTier: string;
format: Format;
prng: PRNG;
noStab: string[];
readonly maxTeamSize: number;
readonly adjustLevel: number | null;
readonly maxMoveCount: number;
readonly forceMonotype: string | undefined;
readonly forceTeraType: string | undefined;
/**
* Checkers for move enforcement based on types or other factors
*
* returns true to try to force the move type, false otherwise.
*/
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);
this.gen = this.dex.gen;
this.noStab = NO_STAB;
const ruleTable = Dex.formats.getRuleTable(format);
this.maxTeamSize = ruleTable.maxTeamSize;
this.adjustLevel = ruleTable.adjustLevel;
this.maxMoveCount = ruleTable.maxMoveCount;
const forceMonotype = ruleTable.valueRules.get('forcemonotype');
this.forceMonotype = forceMonotype && this.dex.types.get(forceMonotype).exists ?
this.dex.types.get(forceMonotype).name : undefined;
const forceTeraType = ruleTable.valueRules.get('forceteratype');
this.forceTeraType = forceTeraType && this.dex.types.get(forceTeraType).exists ?
this.dex.types.get(forceTeraType).name : undefined;
this.factoryTier = '';
this.format = format;
this.prng = PRNG.get(prng);
this.moveEnforcementCheckers = {
Bug: (movePool, moves, abilities, types, counter) => (
movePool.includes('megahorn') || movePool.includes('xscissor') ||
(!counter.get('Bug') && (types.includes('Electric') || types.includes('Psychic')))
),
Dark: (
movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles, teraType, role
) => {
if (
counter.get('Dark') < 2 && PRIORITY_POKEMON.includes(species.id) && role === 'Wallbreaker'
) return true;
return !counter.get('Dark');
},
Dragon: (movePool, moves, abilities, types, counter) => !counter.get('Dragon'),
Electric: (movePool, moves, abilities, types, counter) => !counter.get('Electric'),
Fairy: (movePool, moves, abilities, types, counter) => !counter.get('Fairy'),
Fighting: (movePool, moves, abilities, types, counter) => !counter.get('Fighting'),
Fire: (movePool, moves, abilities, types, counter, species) => !counter.get('Fire'),
Flying: (movePool, moves, abilities, types, counter) => !counter.get('Flying'),
Ghost: (movePool, moves, abilities, types, counter) => !counter.get('Ghost'),
Grass: (movePool, moves, abilities, types, counter, species) => (
!counter.get('Grass') && (
movePool.includes('leafstorm') || species.baseStats.atk >= 100 ||
types.includes('Electric') || abilities.includes('Seed Sower')
)
),
Ground: (movePool, moves, abilities, types, counter) => !counter.get('Ground'),
Ice: (movePool, moves, abilities, types, counter) => (
movePool.includes('freezedry') || movePool.includes('blizzard') || !counter.get('Ice')
),
Normal: (movePool, moves, types, counter) => (movePool.includes('boomburst') || movePool.includes('hypervoice')),
Poison: (movePool, moves, abilities, types, counter) => {
if (types.includes('Ground')) return false;
return !counter.get('Poison');
},
Psychic: (movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles) => {
if ((isDoubles || species.id === 'bruxish') && movePool.includes('psychicfangs')) return true;
if (species.id === 'hoopaunbound' && movePool.includes('psychic')) return true;
if (['Dark', 'Steel', 'Water'].some(m => types.includes(m))) return false;
return !counter.get('Psychic');
},
Rock: (movePool, moves, abilities, types, counter, species) => !counter.get('Rock') && species.baseStats.atk >= 80,
Steel: (movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles) => (
!counter.get('Steel') &&
(isDoubles || species.baseStats.atk >= 90 || movePool.includes('gigatonhammer') || movePool.includes('makeitrain'))
),
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) {
this.prng = PRNG.get(prng);
}
getTeam(options: PlayerOptions | null = null): PokemonSet[] {
const generatorName = (
typeof this.format.team === 'string' && this.format.team.startsWith('random')
) ? this.format.team + 'Team' : '';
// @ts-expect-error property access
return this[generatorName || 'randomTeam'](options);
}
randomChance(numerator: number, denominator: number) {
return this.prng.randomChance(numerator, denominator);
}
sample<T>(items: readonly T[]): T {
return this.prng.sample(items);
}
sampleIfArray<T>(item: T | T[]): T {
if (Array.isArray(item)) {
return this.sample(item);
}
return item;
}
random(m?: number, n?: number) {
return this.prng.random(m, n);
}
/**
* Remove an element from an unsorted array significantly faster
* than .splice
*/
fastPop(list: any[], index: number) {
// If an array doesn't need to be in order, replacing the
// element at the given index with the removed element
// is much, much faster than using list.splice(index, 1).
const length = list.length;
if (index < 0 || index >= list.length) {
// sanity check
throw new Error(`Index ${index} out of bounds for given array`);
}
const element = list[index];
list[index] = list[length - 1];
list.pop();
return element;
}
/**
* Remove a random element from an unsorted array and return it.
* Uses the battle's RNG if in a battle.
*/
sampleNoReplace(list: any[]) {
const length = list.length;
if (length === 0) return null;
const index = this.random(length);
return this.fastPop(list, index);
}
/**
* Removes n random elements from an unsorted array and returns them.
* If n is less than the array's length, randomly removes and returns all the elements
* in the array (so the returned array could have length < n).
*/
multipleSamplesNoReplace<T>(list: T[], n: number): T[] {
const samples = [];
while (samples.length < n && list.length) {
samples.push(this.sampleNoReplace(list));
}
return samples;
}
/**
* Check if user has directly tried to ban/unban/restrict things in a custom battle.
* Doesn't count bans nested inside other formats/rules.
*/
private hasDirectCustomBanlistChanges() {
if (this.format.ruleTable?.has('+pokemontag:cap')) return false;
if (this.format.banlist.length || this.format.restricted.length || this.format.unbanlist.length) return true;
if (!this.format.customRules) return false;
for (const rule of this.format.customRules) {
for (const banlistOperator of ['-', '+', '*']) {
if (rule.startsWith(banlistOperator)) return true;
}
}
return false;
}
/**
* Inform user when custom bans are unsupported in a team generator.
*/
protected enforceNoDirectCustomBanlistChanges() {
if (this.hasDirectCustomBanlistChanges()) {
throw new Error(`Custom bans are not currently supported in ${this.format.name}.`);
}
}
/**
* Inform user when complex bans are unsupported in a team generator.
*/
protected enforceNoDirectComplexBans() {
if (!this.format.customRules) return false;
for (const rule of this.format.customRules) {
if (rule.includes('+') && !rule.startsWith('+')) {
throw new Error(`Complex bans are not currently supported in ${this.format.name}.`);
}
}
}
/**
* Validate set element pool size is sufficient to support size requirements after simple bans.
*/
private enforceCustomPoolSizeNoComplexBans(
effectTypeName: string,
basicEffectPool: BasicEffect[],
requiredCount: number,
requiredCountExplanation: string
) {
if (basicEffectPool.length >= requiredCount) return;
throw new Error(`Legal ${effectTypeName} count is insufficient to support ${requiredCountExplanation} (${basicEffectPool.length} / ${requiredCount}).`);
}
queryMoves(
moves: Set<string> | null,
species: Species,
teraType: string,
abilities: string[],
): MoveCounter {
// This is primarily a helper function for random setbuilder functions.
const counter = new MoveCounter();
const types = species.types;
if (!moves?.size) return counter;
const categories = { Physical: 0, Special: 0, Status: 0 };
// Iterate through all moves we've chosen so far and keep track of what they do:
for (const moveid of moves) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (move.damage || move.damageCallback) {
// Moves that do a set amount of damage:
counter.add('damage');
counter.damagingMoves.add(move);
} else {
// Are Physical/Special/Status moves:
categories[move.category]++;
}
// Moves that have a low base power:
if (moveid === 'lowkick' || (move.basePower && move.basePower <= 60 && moveid !== 'rapidspin')) {
counter.add('technician');
}
// Moves that hit up to 5 times:
if (move.multihit && Array.isArray(move.multihit) && move.multihit[1] === 5) counter.add('skilllink');
if (move.recoil || move.hasCrashDamage) counter.add('recoil');
if (move.drain) counter.add('drain');
// Moves which have a base power:
if (move.basePower || move.basePowerCallback) {
if (!this.noStab.includes(moveid) || PRIORITY_POKEMON.includes(species.id) && move.priority > 0) {
counter.add(moveType);
if (types.includes(moveType)) counter.add('stab');
if (teraType === moveType) counter.add('stabtera');
counter.damagingMoves.add(move);
}
if (move.flags['bite']) counter.add('strongjaw');
if (move.flags['punch']) counter.add('ironfist');
if (move.flags['sound']) counter.add('sound');
if (move.priority > 0 || (moveid === 'grassyglide' && abilities.includes('Grassy Surge'))) {
counter.add('priority');
}
}
// Moves with secondary effects:
if (move.secondary || move.hasSheerForce) {
counter.add('sheerforce');
if (sereneGraceBenefits(move)) {
counter.add('serenegrace');
}
}
// Moves with low accuracy:
if (move.accuracy && move.accuracy !== true && move.accuracy < 90) counter.add('inaccurate');
// Moves that change stats:
if (RECOVERY_MOVES.includes(moveid)) counter.add('recovery');
if (CONTRARY_MOVES.includes(moveid)) counter.add('contrary');
if (PHYSICAL_SETUP.includes(moveid)) counter.add('physicalsetup');
if (SPECIAL_SETUP.includes(moveid)) counter.add('specialsetup');
if (MIXED_SETUP.includes(moveid)) counter.add('mixedsetup');
if (SPEED_SETUP.includes(moveid)) counter.add('speedsetup');
if (SETUP.includes(moveid)) counter.add('setup');
if (HAZARDS.includes(moveid)) counter.add('hazards');
}
counter.set('Physical', Math.floor(categories['Physical']));
counter.set('Special', Math.floor(categories['Special']));
counter.set('Status', categories['Status']);
return counter;
}
cullMovePool(
types: string[],
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
movePool: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
): void {
if (moves.size + movePool.length <= this.maxMoveCount) return;
// If we have two unfilled moves and only one unpaired move, cull the unpaired move.
if (moves.size === this.maxMoveCount - 2) {
const unpairedMoves = [...movePool];
for (const pair of MOVE_PAIRS) {
if (movePool.includes(pair[0]) && movePool.includes(pair[1])) {
this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[0]));
this.fastPop(unpairedMoves, unpairedMoves.indexOf(pair[1]));
}
}
if (unpairedMoves.length === 1) {
this.fastPop(movePool, movePool.indexOf(unpairedMoves[0]));
}
}
// These moves are paired, and shouldn't appear if there is not room for them both.
if (moves.size === this.maxMoveCount - 1) {
for (const pair of MOVE_PAIRS) {
if (movePool.includes(pair[0]) && movePool.includes(pair[1])) {
this.fastPop(movePool, movePool.indexOf(pair[0]));
this.fastPop(movePool, movePool.indexOf(pair[1]));
}
}
}
// Develop additional move lists
const statusMoves = this.cachedStatusMoves;
// Team-based move culls
if (teamDetails.screens) {
if (movePool.includes('auroraveil')) this.fastPop(movePool, movePool.indexOf('auroraveil'));
if (movePool.length >= this.maxMoveCount + 2) {
if (movePool.includes('reflect')) this.fastPop(movePool, movePool.indexOf('reflect'));
if (movePool.includes('lightscreen')) this.fastPop(movePool, movePool.indexOf('lightscreen'));
}
}
if (teamDetails.stickyWeb) {
if (movePool.includes('stickyweb')) this.fastPop(movePool, movePool.indexOf('stickyweb'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (teamDetails.stealthRock) {
if (movePool.includes('stealthrock')) this.fastPop(movePool, movePool.indexOf('stealthrock'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (teamDetails.defog || teamDetails.rapidSpin) {
if (movePool.includes('defog')) this.fastPop(movePool, movePool.indexOf('defog'));
if (movePool.includes('rapidspin')) this.fastPop(movePool, movePool.indexOf('rapidspin'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (teamDetails.toxicSpikes) {
if (movePool.includes('toxicspikes')) this.fastPop(movePool, movePool.indexOf('toxicspikes'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (teamDetails.spikes && teamDetails.spikes >= 2) {
if (movePool.includes('spikes')) this.fastPop(movePool, movePool.indexOf('spikes'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (teamDetails.statusCure) {
if (movePool.includes('healbell')) this.fastPop(movePool, movePool.indexOf('healbell'));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
if (isDoubles) {
const doublesIncompatiblePairs = [
// In order of decreasing generalizability
[SPEED_CONTROL, SPEED_CONTROL],
[HAZARDS, HAZARDS],
['rockslide', 'stoneedge'],
[SETUP, ['fakeout', 'helpinghand']],
[PROTECT_MOVES, 'wideguard'],
[['fierydance', 'fireblast'], 'heatwave'],
['dazzlinggleam', ['fleurcannon', 'moonblast']],
['poisongas', ['toxicspikes', 'willowisp']],
[RECOVERY_MOVES, 'healpulse'],
['lifedew', 'healpulse'],
['haze', 'icywind'],
[['hydropump', 'muddywater'], ['muddywater', 'scald']],
['disable', 'encore'],
['freezedry', 'icebeam'],
['energyball', 'leafstorm'],
['wildcharge', 'thunderbolt'],
['earthpower', 'sandsearstorm'],
['coaching', ['helpinghand', 'howl']],
];
for (const pair of doublesIncompatiblePairs) this.incompatibleMoves(moves, movePool, pair[0], pair[1]);
if (role !== 'Offensive Protect') this.incompatibleMoves(moves, movePool, PROTECT_MOVES, ['flipturn', 'uturn']);
}
// General incompatibilities
const incompatiblePairs = [
// These moves don't mesh well with other aspects of the set
[statusMoves, ['healingwish', 'switcheroo', 'trick']],
[SETUP, PIVOT_MOVES],
[SETUP, HAZARDS],
[SETUP, ['defog', 'nuzzle', 'toxic', 'yawn', 'haze']],
[PHYSICAL_SETUP, PHYSICAL_SETUP],
[SPECIAL_SETUP, 'thunderwave'],
['substitute', PIVOT_MOVES],
[SPEED_SETUP, ['aquajet', 'rest', 'trickroom']],
['curse', ['irondefense', 'rapidspin']],
['dragondance', 'dracometeor'],
['yawn', 'roar'],
// These attacks are redundant with each other
[['psychic', 'psychicnoise'], ['psyshock', 'psychicnoise']],
['surf', 'hydropump'],
['liquidation', 'wavecrash'],
['aquajet', 'flipturn'],
['gigadrain', 'leafstorm'],
['powerwhip', 'hornleech'],
[['airslash', 'bravebird', 'hurricane'], ['airslash', 'bravebird', 'hurricane']],
['knockoff', 'foulplay'],
['throatchop', ['crunch', 'lashout']],
['doubleedge', ['bodyslam', 'headbutt']],
['fireblast', ['fierydance', 'flamethrower']],
['lavaplume', 'magmastorm'],
['thunderpunch', 'wildcharge'],
['thunderbolt', 'discharge'],
['gunkshot', ['direclaw', 'poisonjab', 'sludgebomb']],
['aurasphere', 'focusblast'],
['closecombat', 'drainpunch'],
['bugbite', 'pounce'],
[['dragonpulse', 'spacialrend'], 'dracometeor'],
['heavyslam', 'flashcannon'],
['alluringvoice', 'dazzlinggleam'],
// These status moves are redundant with each other
['taunt', 'disable'],
[['thunderwave', 'toxic'], ['thunderwave', 'willowisp']],
[['thunderwave', 'toxic', 'willowisp'], 'toxicspikes'],
// This space reserved for assorted hardcodes that otherwise make little sense out of context
// Landorus and Thundurus
['nastyplot', ['rockslide', 'knockoff']],
// Persian
['switcheroo', 'fakeout'],
// Amoonguss, though this can work well as a general rule later
['toxic', 'clearsmog'],
// Chansey and Blissey
['healbell', 'stealthrock'],
// Azelf and Zoroarks
['trick', 'uturn'],
// Araquanid
['mirrorcoat', 'hydropump'],
];
for (const pair of incompatiblePairs) this.incompatibleMoves(moves, movePool, pair[0], pair[1]);
if (!types.includes('Ice')) this.incompatibleMoves(moves, movePool, 'icebeam', 'icywind');
if (!isDoubles) this.incompatibleMoves(moves, movePool, 'taunt', 'encore');
if (!types.includes('Dark') && teraType !== 'Dark') this.incompatibleMoves(moves, movePool, 'knockoff', 'suckerpunch');
if (!abilities.includes('Prankster')) this.incompatibleMoves(moves, movePool, 'thunderwave', 'yawn');
// This space reserved for assorted hardcodes that otherwise make little sense out of context
if (species.id === 'barraskewda') {
this.incompatibleMoves(moves, movePool, ['psychicfangs', 'throatchop'], ['poisonjab', 'throatchop']);
}
if (species.id === 'cyclizar') this.incompatibleMoves(moves, movePool, 'taunt', 'knockoff');
if (species.id === 'camerupt') this.incompatibleMoves(moves, movePool, 'roar', 'willowisp');
if (species.id === 'coalossal') this.incompatibleMoves(moves, movePool, 'flamethrower', 'overheat');
}
// Checks for and removes incompatible moves, starting with the first move in movesA.
incompatibleMoves(
moves: Set<string>,
movePool: string[],
movesA: string | string[],
movesB: string | string[],
): void {
const moveArrayA = (Array.isArray(movesA)) ? movesA : [movesA];
const moveArrayB = (Array.isArray(movesB)) ? movesB : [movesB];
if (moves.size + movePool.length <= this.maxMoveCount) return;
for (const moveid1 of moves) {
if (moveArrayB.includes(moveid1)) {
for (const moveid2 of moveArrayA) {
if (moveid1 !== moveid2 && movePool.includes(moveid2)) {
this.fastPop(movePool, movePool.indexOf(moveid2));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
}
}
if (moveArrayA.includes(moveid1)) {
for (const moveid2 of moveArrayB) {
if (moveid1 !== moveid2 && movePool.includes(moveid2)) {
this.fastPop(movePool, movePool.indexOf(moveid2));
if (moves.size + movePool.length <= this.maxMoveCount) return;
}
}
}
}
}
// Adds a move to the moveset, returns the MoveCounter
addMove(
move: string,
moves: Set<string>,
types: string[],
abilities: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
movePool: string[],
teraType: string,
role: RandomTeamsTypes.Role,
): MoveCounter {
moves.add(move);
this.fastPop(movePool, movePool.indexOf(move));
const counter = this.queryMoves(moves, species, teraType, abilities);
this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role);
return counter;
}
// Returns the type of a given move for STAB/coverage enforcement purposes
getMoveType(move: Move, species: Species, abilities: string[], teraType: string): string {
if (move.id === 'terablast') return teraType;
if (['judgment', 'revelationdance'].includes(move.id)) return species.types[0];
if (move.name === "Raging Bull" && species.name.startsWith("Tauros-Paldea")) {
if (species.name.endsWith("Combat")) return "Fighting";
if (species.name.endsWith("Blaze")) return "Fire";
if (species.name.endsWith("Aqua")) return "Water";
}
if (move.name === "Ivy Cudgel" && species.name.startsWith("Ogerpon")) {
if (species.name.endsWith("Wellspring")) return "Water";
if (species.name.endsWith("Hearthflame")) return "Fire";
if (species.name.endsWith("Cornerstone")) return "Rock";
}
const moveType = move.type;
if (moveType === 'Normal') {
if (abilities.includes('Aerilate')) return 'Flying';
if (abilities.includes('Galvanize')) return 'Electric';
if (abilities.includes('Pixilate')) return 'Fairy';
if (abilities.includes('Refrigerate')) return 'Ice';
}
return moveType;
}
// Generate random moveset for a given species, role, tera type.
randomMoveset(
types: string[],
abilities: string[],
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
movePool: string[],
teraType: string,
role: RandomTeamsTypes.Role,
): Set<string> {
const moves = new Set<string>();
let counter = this.queryMoves(moves, species, teraType, abilities);
this.cullMovePool(types, moves, abilities, counter, movePool, teamDetails, species, isLead, isDoubles, teraType, role);
// If there are only four moves, add all moves and return early
if (movePool.length <= this.maxMoveCount) {
for (const moveid of movePool) {
moves.add(moveid);
}
return moves;
}
const runEnforcementChecker = (checkerName: string) => {
if (!this.moveEnforcementCheckers[checkerName]) return false;
return this.moveEnforcementCheckers[checkerName](
movePool, moves, abilities, types, counter, species, teamDetails, isLead, isDoubles, teraType, role
);
};
if (role === 'Tera Blast user') {
counter = this.addMove('terablast', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Add required move (e.g. Relic Song for Meloetta-P)
if (species.requiredMove) {
const move = this.dex.moves.get(species.requiredMove).id;
counter = this.addMove(move, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Add other moves you really want to have, e.g. STAB, recovery, setup.
// Enforce Facade if Guts is a possible ability
if (movePool.includes('facade') && abilities.includes('Guts')) {
counter = this.addMove('facade', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Enforce Night Shade, Revelation Dance, Revival Blessing, and Sticky Web
for (const moveid of ['nightshade', 'revelationdance', 'revivalblessing', 'stickyweb']) {
if (movePool.includes(moveid)) {
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Trick Room on Doubles Wallbreaker
if (movePool.includes('trickroom') && role === 'Doubles Wallbreaker') {
counter = this.addMove('trickroom', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Enforce hazard removal on Bulky Support if the team doesn't already have it
if (role === 'Bulky Support' && !teamDetails.defog && !teamDetails.rapidSpin) {
if (movePool.includes('rapidspin')) {
counter = this.addMove('rapidspin', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
if (movePool.includes('defog')) {
counter = this.addMove('defog', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Aurora Veil if the team doesn't already have screens
if (!teamDetails.screens && movePool.includes('auroraveil')) {
counter = this.addMove('auroraveil', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Enforce Knock Off on pure Normal- and Fighting-types in singles
if (!isDoubles && types.length === 1 && (types.includes('Normal') || types.includes('Fighting'))) {
if (movePool.includes('knockoff')) {
counter = this.addMove('knockoff', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Spore on Smeargle
if (species.id === 'smeargle') {
if (movePool.includes('spore')) {
counter = this.addMove('spore', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce moves in doubles
if (isDoubles) {
const doublesEnforcedMoves = ['mortalspin', 'spore'];
for (const moveid of doublesEnforcedMoves) {
if (movePool.includes(moveid)) {
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Fake Out on slow Pokemon
if (movePool.includes('fakeout') && species.baseStats.spe <= 50) {
counter = this.addMove('fakeout', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Enforce Tailwind on Prankster and Gale Wings users
if (movePool.includes('tailwind') && (abilities.includes('Prankster') || abilities.includes('Gale Wings'))) {
counter = this.addMove('tailwind', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
// Enforce Thunder Wave on Prankster users as well
if (movePool.includes('thunderwave') && abilities.includes('Prankster')) {
counter = this.addMove('thunderwave', moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce STAB priority
if (
['Bulky Attacker', 'Bulky Setup', 'Wallbreaker', 'Doubles Wallbreaker'].includes(role) ||
PRIORITY_POKEMON.includes(species.id)
) {
const priorityMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (
types.includes(moveType) && (move.priority > 0 || (moveid === 'grassyglide' && abilities.includes('Grassy Surge'))) &&
(move.basePower || move.basePowerCallback)
) {
priorityMoves.push(moveid);
}
}
if (priorityMoves.length) {
const moveid = this.sample(priorityMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce STAB
for (const type of types) {
// Check if a STAB move of that type should be required
const stabMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && type === moveType) {
stabMoves.push(moveid);
}
}
while (runEnforcementChecker(type)) {
if (!stabMoves.length) break;
const moveid = this.sampleNoReplace(stabMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Tera STAB
if (!counter.get('stabtera') && !['Bulky Support', 'Doubles Support'].includes(role)) {
const stabMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && teraType === moveType) {
stabMoves.push(moveid);
}
}
if (stabMoves.length) {
const moveid = this.sample(stabMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// If no STAB move was added, add a STAB move
if (!counter.get('stab')) {
const stabMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback) && types.includes(moveType)) {
stabMoves.push(moveid);
}
}
if (stabMoves.length) {
const moveid = this.sample(stabMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce recovery
if (['Bulky Support', 'Bulky Attacker', 'Bulky Setup'].includes(role)) {
const recoveryMoves = movePool.filter(moveid => RECOVERY_MOVES.includes(moveid));
if (recoveryMoves.length) {
const moveid = this.sample(recoveryMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce pivoting moves on AV Pivot
if (role === 'AV Pivot') {
const pivotMoves = movePool.filter(moveid => ['uturn', 'voltswitch'].includes(moveid));
if (pivotMoves.length) {
const moveid = this.sample(pivotMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce setup
if (role.includes('Setup') || role === 'Tera Blast user') {
// First, try to add a non-Speed setup move
const nonSpeedSetupMoves = movePool.filter(moveid => SETUP.includes(moveid) && !SPEED_SETUP.includes(moveid));
if (nonSpeedSetupMoves.length) {
const moveid = this.sample(nonSpeedSetupMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
} else {
// No non-Speed setup moves, so add any (Speed) setup move
const setupMoves = movePool.filter(moveid => SETUP.includes(moveid));
if (setupMoves.length) {
const moveid = this.sample(setupMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
}
// Enforce redirecting moves and Fake Out on Doubles Support
if (role === 'Doubles Support') {
for (const moveid of ['fakeout', 'followme', 'ragepowder']) {
if (movePool.includes(moveid)) {
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
const speedControl = movePool.filter(moveid => SPEED_CONTROL.includes(moveid));
if (speedControl.length) {
const moveid = this.sample(speedControl);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce Protect
if (role.includes('Protect')) {
const protectMoves = movePool.filter(moveid => PROTECT_MOVES.includes(moveid));
if (protectMoves.length) {
const moveid = this.sample(protectMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce a move not on the noSTAB list
if (!counter.damagingMoves.size) {
// Choose an attacking move
const attackingMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
if (!this.noStab.includes(moveid) && (move.category !== 'Status')) attackingMoves.push(moveid);
}
if (attackingMoves.length) {
const moveid = this.sample(attackingMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
// Enforce coverage move
if (!['AV Pivot', 'Fast Support', 'Bulky Support', 'Bulky Protect', 'Doubles Support'].includes(role)) {
if (counter.damagingMoves.size === 1) {
// Find the type of the current attacking move
const currentAttackType = counter.damagingMoves.values().next().value!.type;
// Choose an attacking move that is of different type to the current single attack
const coverageMoves = [];
for (const moveid of movePool) {
const move = this.dex.moves.get(moveid);
const moveType = this.getMoveType(move, species, abilities, teraType);
if (!this.noStab.includes(moveid) && (move.basePower || move.basePowerCallback)) {
if (currentAttackType !== moveType) coverageMoves.push(moveid);
}
}
if (coverageMoves.length) {
const moveid = this.sample(coverageMoves);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
}
// Add (moves.size < this.maxMoveCount) as a condition if moves is getting larger than 4 moves.
// If you want moves to be favored but not required, add something like && this.randomChance(1, 2) to your condition.
// Choose remaining moves randomly from movepool and add them to moves list:
while (moves.size < this.maxMoveCount && movePool.length) {
if (moves.size + movePool.length <= this.maxMoveCount) {
for (const moveid of movePool) {
moves.add(moveid);
}
break;
}
const moveid = this.sample(movePool);
counter = this.addMove(moveid, moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
for (const pair of MOVE_PAIRS) {
if (moveid === pair[0] && movePool.includes(pair[1])) {
counter = this.addMove(pair[1], moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
if (moveid === pair[1] && movePool.includes(pair[0])) {
counter = this.addMove(pair[0], moves, types, abilities, teamDetails, species, isLead, isDoubles,
movePool, teraType, role);
}
}
}
return moves;
}
shouldCullAbility(
ability: string,
types: string[],
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
): boolean {
switch (ability) {
// Abilities which are primarily useful for certain moves or with team support
case 'Chlorophyll': case 'Solar Power':
return !teamDetails.sun;
case 'Defiant':
return (species.id === 'thundurus' && !!counter.get('Status'));
case 'Hydration': case 'Swift Swim':
return !teamDetails.rain;
case 'Iron Fist': case 'Skill Link':
return !counter.get(toID(ability));
case 'Overgrow':
return !counter.get('Grass');
case 'Prankster':
return !counter.get('Status');
case 'Sand Force': case 'Sand Rush':
return !teamDetails.sand;
case 'Slush Rush':
return !teamDetails.snow;
case 'Swarm':
return !counter.get('Bug');
case 'Torrent':
return (!counter.get('Water') && !moves.has('flipturn'));
}
return false;
}
getAbility(
types: string[],
moves: Set<string>,
abilities: string[],
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
): string {
// ffa abilities that differ from doubles
if (this.format.gameType === 'freeforall') {
if (species.id === 'bellossom') return 'Chlorophyll';
if (species.id === 'sinistcha') return 'Heatproof';
if (abilities.length === 1 && abilities[0] === 'Telepathy') {
return species.id === 'oranguru' ? 'Inner Focus' : 'Pressure';
}
if (species.id === 'duraludon') return 'Light Metal';
if (species.id === 'clefairy') return 'Magic Guard';
if (species.id === 'blissey') return 'Natural Cure';
if (species.id === 'barraskewda') return 'Swift Swim';
}
if (abilities.length <= 1) return abilities[0];
// Hard-code abilities here
if (species.id === 'drifblim') return moves.has('defog') ? 'Aftermath' : 'Unburden';
if (abilities.includes('Flash Fire') && this.dex.getEffectiveness('Fire', teraType) >= 1) return 'Flash Fire';
if (species.id === 'hitmonchan' && counter.get('ironfist')) return 'Iron Fist';
if ((species.id === 'thundurus' || species.id === 'tornadus') && !counter.get('Physical')) return 'Prankster';
if (species.id === 'swampert' && (counter.get('Water') || moves.has('flipturn'))) return 'Torrent';
if (species.id === 'toucannon' && counter.get('skilllink')) return 'Skill Link';
if (abilities.includes('Slush Rush') && moves.has('snowscape')) return 'Slush Rush';
if (species.id === 'golduck' && teamDetails.rain) return 'Swift Swim';
const abilityAllowed: string[] = [];
// Obtain a list of abilities that are allowed (not culled)
for (const ability of abilities) {
if (!this.shouldCullAbility(
ability, types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, teraType, role
)) {
abilityAllowed.push(ability);
}
}
// Pick a random allowed ability
if (abilityAllowed.length >= 1) return this.sample(abilityAllowed);
// If all abilities are rejected, prioritize weather abilities over non-weather abilities
if (!abilityAllowed.length) {
const weatherAbilities = abilities.filter(
a => ['Chlorophyll', 'Hydration', 'Sand Force', 'Sand Rush', 'Slush Rush', 'Solar Power', 'Swift Swim'].includes(a)
);
if (weatherAbilities.length) return this.sample(weatherAbilities);
}
// Pick a random ability
return this.sample(abilities);
}
getPriorityItem(
ability: string,
types: string[],
moves: Set<string>,
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
isDoubles: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
) {
if (!isDoubles) {
if (role === 'Fast Bulky Setup' && (ability === 'Quark Drive' || ability === 'Protosynthesis')) {
return 'Booster Energy';
}
if (species.id === 'lokix') {
return (role === 'Fast Attacker') ? 'Silver Powder' : 'Life Orb';
}
}
if (species.requiredItems) {
// Z-Crystals aren't available in Gen 9, so require Plates
if (species.baseSpecies === 'Arceus') {
return species.requiredItems[0];
}
return this.sample(species.requiredItems);
}
if (role === 'AV Pivot') return 'Assault Vest';
if (species.id === 'pikachu') return 'Light Ball';
if (species.id === 'regieleki') return 'Magnet';
if (types.includes('Normal') && moves.has('doubleedge') && moves.has('fakeout')) return 'Silk Scarf';
if (
species.id === 'froslass' || moves.has('populationbomb') ||
(ability === 'Hustle' && counter.get('setup') && !isDoubles && this.randomChance(1, 2))
) return 'Wide Lens';
if (species.id === 'smeargle' && !isDoubles) return 'Focus Sash';
if (moves.has('clangoroussoul') || (species.id === 'toxtricity' && moves.has('shiftgear'))) return 'Throat Spray';
if (
(species.baseSpecies === 'Magearna' && role === 'Tera Blast user') ||
species.id === 'necrozmaduskmane' || (species.id === 'calyrexice' && isDoubles)
) return 'Weakness Policy';
if (['dragonenergy', 'lastrespects', 'waterspout'].some(m => moves.has(m))) return 'Choice Scarf';
if (
!isDoubles && (ability === 'Imposter' || (species.id === 'magnezone' && role === 'Fast Attacker'))
) return 'Choice Scarf';
if (species.id === 'rampardos' && (role === 'Fast Attacker' || isDoubles)) return 'Choice Scarf';
if (species.id === 'palkia' && counter.get('Special') < 4) return 'Lustrous Orb';
if (
moves.has('courtchange') ||
!isDoubles && (species.id === 'luvdisc' || (species.id === 'terapagos' && !moves.has('rest')))
) return 'Heavy-Duty Boots';
if (moves.has('bellydrum') && moves.has('substitute')) return 'Salac Berry';
if (
['Cheek Pouch', 'Cud Chew', 'Harvest', 'Ripen'].some(m => ability === m) ||
moves.has('bellydrum') || moves.has('filletaway')
) {
return 'Sitrus Berry';
}
if (['healingwish', 'switcheroo', 'trick'].some(m => moves.has(m))) {
if (
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 &&
role !== 'Wallbreaker' && role !== 'Doubles Wallbreaker' && !counter.get('priority')
) {
return 'Choice Scarf';
} else {
return (counter.get('Physical') > counter.get('Special')) ? 'Choice Band' : 'Choice Specs';
}
}
if (counter.get('Status') && (species.name === 'Latias' || species.name === 'Latios')) return 'Soul Dew';
if (species.id === 'scyther' && !isDoubles) return (isLead && !moves.has('uturn')) ? 'Eviolite' : 'Heavy-Duty Boots';
if (ability === 'Poison Heal' || ability === 'Quick Feet') return 'Toxic Orb';
if (species.nfe) return 'Eviolite';
if ((ability === 'Guts' || moves.has('facade')) && !moves.has('sleeptalk')) {
return (types.includes('Fire') || ability === 'Toxic Boost') ? 'Toxic Orb' : 'Flame Orb';
}
if (ability === 'Magic Guard' || (ability === 'Sheer Force' && counter.get('sheerforce'))) return 'Life Orb';
if (ability === 'Anger Shell') return this.sample(['Rindo Berry', 'Passho Berry', 'Scope Lens', 'Sitrus Berry']);
if (moves.has('dragondance') && isDoubles) return 'Clear Amulet';
if (counter.get('skilllink') && ability !== 'Skill Link' && species.id !== 'breloom') return 'Loaded Dice';
if (ability === 'Unburden') {
return (moves.has('closecombat') || moves.has('leafstorm')) ? 'White Herb' : 'Sitrus Berry';
}
if (moves.has('shellsmash') && ability !== 'Weak Armor') return 'White Herb';
if (moves.has('meteorbeam') || (moves.has('electroshot') && !teamDetails.rain)) return 'Power Herb';
if (moves.has('acrobatics') && ability !== 'Protosynthesis') return '';
if (moves.has('auroraveil') || moves.has('lightscreen') && moves.has('reflect')) return 'Light Clay';
if (ability === 'Gluttony') return `${this.sample(['Aguav', 'Figy', 'Iapapa', 'Mago', 'Wiki'])} Berry`;
if (
moves.has('rest') && !moves.has('sleeptalk') &&
ability !== 'Natural Cure' && ability !== 'Shed Skin'
) {
return 'Chesto Berry';
}
if (
species.id !== 'yanmega' &&
this.dex.getEffectiveness('Rock', species) >= 2 && (!types.includes('Flying') || !isDoubles)
) return 'Heavy-Duty Boots';
}
/** Item generation specific to Random Doubles */
getDoublesItem(
ability: string,
types: string[],
moves: Set<string>,
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
): string {
const scarfReqs = (
!counter.get('priority') && ability !== 'Speed Boost' && role !== 'Doubles Wallbreaker' &&
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 &&
this.randomChance(1, 2)
);
const offensiveRole = (
['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Doubles Setup Sweeper', 'Offensive Protect'].some(m => role === m)
);
const doublesLeftoversHardcodes = (
moves.has('acidarmor') || species.id === 'eternatus' || species.id === 'regigigas' || moves.has('wish')
);
if (species.id === 'ursalunabloodmoon' && moves.has('protect')) return 'Silk Scarf';
if (
moves.has('flipturn') && moves.has('protect') && (moves.has('aquajet') || (moves.has('jetpunch')))
) return 'Mystic Water';
if (counter.get('speedsetup') && role === 'Doubles Bulky Setup') return 'Weakness Policy';
if (species.id === 'toxapex') return 'Binding Band';
if (moves.has('blizzard') && ability !== 'Snow Warning' && !teamDetails.snow) return 'Blunder Policy';
if (role === 'Choice Item user') {
if (scarfReqs || (counter.get('Physical') < 4 && counter.get('Special') < 3 && !moves.has('memento'))) {
return 'Choice Scarf';
}
return (counter.get('Physical') >= 3) ? 'Choice Band' : 'Choice Specs';
}
if (counter.get('Physical') >= 4 &&
['fakeout', 'feint', 'firstimpression', 'rapidspin', 'suckerpunch'].every(m => !moves.has(m)) &&
(moves.has('flipturn') || moves.has('uturn') || role === 'Doubles Wallbreaker')
) {
return (scarfReqs) ? 'Choice Scarf' : 'Choice Band';
}
if (
((counter.get('Special') >= 4 && (moves.has('voltswitch') || role === 'Doubles Wallbreaker')) || (
counter.get('Special') >= 3 && (moves.has('uturn') || moves.has('flipturn'))
)) && !moves.has('electroweb')
) {
return (scarfReqs) ? 'Choice Scarf' : 'Choice Specs';
}
if (
(role === 'Bulky Protect' && counter.get('setup')) || moves.has('substitute') || moves.has('irondefense') ||
moves.has('coil') || doublesLeftoversHardcodes
) return 'Leftovers';
if (species.id === 'sylveon') return 'Pixie Plate';
if (ability === 'Intimidate' && this.dex.getEffectiveness('Rock', species) >= 1) return 'Heavy-Duty Boots';
if (
(offensiveRole || (role === 'Tera Blast user' && (species.baseStats.spe >= 80 || moves.has('trickroom')))) &&
(!moves.has('fakeout') || species.id === 'ambipom') && !moves.has('incinerate') &&
(!moves.has('uturn') || types.includes('Bug') || ability === 'Libero') &&
((!moves.has('icywind') && !moves.has('electroweb')) || species.id === 'ironbundle')
) {
return (
(ability === 'Quark Drive' || ability === 'Protosynthesis') && !isLead && species.id !== 'ironvaliant' &&
['dracometeor', 'firstimpression', 'uturn', 'voltswitch'].every(m => !moves.has(m))
) ? 'Booster Energy' : 'Life Orb';
}
if (isLead && (species.id === 'glimmora' ||
(['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Offensive Protect'].includes(role) &&
species.baseStats.hp + species.baseStats.def + species.baseStats.spd <= 230))
) return 'Focus Sash';
if (
['Doubles Fast Attacker', 'Doubles Wallbreaker', 'Offensive Protect'].includes(role) &&
moves.has('fakeout') || moves.has('incinerate')
) {
return (this.dex.getEffectiveness('Rock', species) >= 1) ? 'Heavy-Duty Boots' : 'Clear Amulet';
}
if (!counter.get('Status')) return 'Assault Vest';
return 'Sitrus Berry';
}
getItem(
ability: string,
types: string[],
moves: Set<string>,
counter: MoveCounter,
teamDetails: RandomTeamsTypes.TeamDetails,
species: Species,
isLead: boolean,
teraType: string,
role: RandomTeamsTypes.Role,
): string {
if (
species.id !== 'jirachi' && (counter.get('Physical') >= 4) &&
['dragontail', 'fakeout', 'firstimpression', 'flamecharge', 'rapidspin'].every(m => !moves.has(m))
) {
const scarfReqs = (
role !== 'Wallbreaker' &&
(species.baseStats.atk >= 100 || ability === 'Huge Power' || ability === 'Pure Power') &&
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 &&
ability !== 'Speed Boost' && !counter.get('priority') && !moves.has('aquastep')
);
return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Band';
}
if (
(counter.get('Special') >= 4) ||
(counter.get('Special') >= 3 && ['flipturn', 'uturn'].some(m => moves.has(m)))
) {
const scarfReqs = (
role !== 'Wallbreaker' &&
species.baseStats.spa >= 100 &&
species.baseStats.spe >= 60 && species.baseStats.spe <= 108 &&
ability !== 'Speed Boost' && ability !== 'Tinted Lens' && !moves.has('uturn') && !counter.get('priority')
);
return (scarfReqs && this.randomChance(1, 2)) ? 'Choice Scarf' : 'Choice Specs';
}
if (counter.get('speedsetup') && role === 'Bulky Setup') return 'Weakness Policy';
if (
!counter.get('Status') &&
!['Fast Attacker', 'Wallbreaker', 'Tera Blast user'].includes(role)
) {
return 'Assault Vest';
}
if (species.id === 'golem') return (counter.get('speedsetup')) ? 'Weakness Policy' : 'Custap Berry';
if (moves.has('substitute')) return 'Leftovers';
if (
moves.has('stickyweb') && isLead &&
(species.baseStats.hp + species.baseStats.def + species.baseStats.spd) <= 235
) return 'Focus Sash';
if (this.dex.getEffectiveness('Rock', species) >= 1) return 'Heavy-Duty Boots';
if (
(moves.has('chillyreception') || (
role === 'Fast Support' &&
[...PIVOT_MOVES, 'defog', 'mortalspin', 'rapidspin'].some(m => moves.has(m)) &&
!types.includes('Flying') && ability !== 'Levitate'
))
) return 'Heavy-Duty Boots';
// Low Priority
if (
ability === 'Rough Skin' || (
ability === 'Regenerator' && (role === 'Bulky Support' || role === 'Bulky Attacker') &&
(species.baseStats.hp + species.baseStats.def) >= 180 && this.randomChance(1, 2)
) || (
ability !== 'Regenerator' && !counter.get('setup') && counter.get('recovery') &&
this.dex.getEffectiveness('Fighting', species) < 1 &&
(species.baseStats.hp + species.baseStats.def) > 200 && this.randomChance(1, 2)
)
) return 'Rocky Helmet';
if (moves.has('outrage') && counter.get('setup')) return 'Lum Berry';
if (moves.has('protect') && ability !== 'Speed Boost') return 'Leftovers';
if (
role === 'Fast Support' && isLead && !counter.get('recovery') && !counter.get('recoil') &&
(counter.get('hazards') || counter.get('setup')) &&
(species.baseStats.hp + species.baseStats.def + species.baseStats.spd) < 258
) return 'Focus Sash';
if (
!counter.get('setup') && ability !== 'Levitate' && this.dex.getEffectiveness('Ground', species) >= 2
) return 'Air Balloon';
if (['Bulky Attacker', 'Bulky Support', 'Bulky Setup'].some(m => role === (m))) return 'Leftovers';
if (species.id === 'pawmot' && moves.has('nuzzle')) return 'Leppa Berry';
if (role === 'Fast Support' || role === 'Fast Bulky Setup') {
return (counter.get('Physical') + counter.get('Special') >= 3 && !moves.has('nuzzle')) ? 'Life Orb' : 'Leftovers';
}
if (role === 'Tera Blast user' && DEFENSIVE_TERA_BLAST_USERS.includes(species.id)) return 'Leftovers';
if (
['flamecharge', 'rapidspin', 'trailblaze'].every(m => !moves.has(m)) &&
['Fast Attacker', 'Setup Sweeper', 'Tera Blast user', 'Wallbreaker'].some(m => role === (m))
) return 'Life Orb';
return 'Leftovers';
}
getLevel(
species: Species,
isDoubles: boolean,
): number {
if (this.adjustLevel) return this.adjustLevel;
// doubles levelling
if (isDoubles && this.randomDoublesSets[species.id]["level"]) return this.randomDoublesSets[species.id]["level"]!;
if (!isDoubles && this.randomSets[species.id]["level"]) return this.randomSets[species.id]["level"]!;
// Default to tier-based levelling
const tier = species.tier;
const tierScale: Partial<Record<Species['tier'], number>> = {
Uber: 76,
OU: 80,
UUBL: 81,
UU: 82,
RUBL: 83,
RU: 84,
NUBL: 85,
NU: 86,
PUBL: 87,
PU: 88, "(PU)": 88, NFE: 88,
};
return tierScale[tier] || 80;
}
getForme(species: Species): string {
if (typeof species.battleOnly === 'string') {
// Only change the forme. The species has custom moves, and may have different typing and requirements.
return species.battleOnly;
}
if (species.cosmeticFormes) return this.sample([species.name].concat(species.cosmeticFormes));
// Consolidate mostly-cosmetic formes, at least for the purposes of Random Battles
if (['Dudunsparce', 'Magearna', 'Maushold', 'Polteageist', 'Sinistcha', 'Zarude'].includes(species.baseSpecies)) {
return this.sample([species.name].concat(species.otherFormes!));
}
if (species.baseSpecies === 'Basculin') return 'Basculin' + this.sample(['', '-Blue-Striped']);
if (species.baseSpecies === 'Pikachu') {
return 'Pikachu' + this.sample(
['', '-Original', '-Hoenn', '-Sinnoh', '-Unova', '-Kalos', '-Alola', '-Partner', '-World']
);
}
return species.name;
}
randomSet(
s: string | Species,
teamDetails: RandomTeamsTypes.TeamDetails = {},
isLead = false,
isDoubles = false
): RandomTeamsTypes.RandomSet {
const species = this.dex.species.get(s);
const forme = this.getForme(species);
const sets = this[`random${isDoubles ? 'Doubles' : ''}Sets`][species.id]["sets"];
const possibleSets: RandomTeamsTypes.RandomSetData[] = [];
const ruleTable = this.dex.formats.getRuleTable(this.format);
for (const set of sets) {
// Prevent Fast Bulky Setup on lead Paradox Pokemon, since it generates Booster Energy.
const abilities = set.abilities!;
if (
isLead && (abilities.includes('Protosynthesis') || abilities.includes('Quark Drive')) &&
set.role === 'Fast Bulky Setup'
) continue;
// Prevent Tera Blast user if the team already has one, or if Terastallizion is prevented.
if ((teamDetails.teraBlast || ruleTable.has('terastalclause')) && set.role === 'Tera Blast user') {
continue;
}
possibleSets.push(set);
}
const set = this.sampleIfArray(possibleSets);
const role = set.role;
const movePool: string[] = [];
for (const movename of set.movepool) {
movePool.push(this.dex.moves.get(movename).id);
}
const teraTypes = set.teraTypes!;
let teraType = this.sampleIfArray(teraTypes);
let ability = '';
let item = undefined;
const evs = { hp: 85, atk: 85, def: 85, spa: 85, spd: 85, spe: 85 };
const ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
const types = species.types;
const abilities = set.abilities!;
// Get moves
const moves = this.randomMoveset(types, abilities, teamDetails, species, isLead, isDoubles, movePool, teraType, role);
const counter = this.queryMoves(moves, species, teraType, abilities);
// Get ability
ability = this.getAbility(types, moves, abilities, counter, teamDetails, species, isLead, isDoubles, teraType, role);
// Get items
// First, the priority items
item = this.getPriorityItem(ability, types, moves, counter, teamDetails, species, isLead, isDoubles, teraType, role);
if (item === undefined) {
if (isDoubles) {
item = this.getDoublesItem(ability, types, moves, counter, teamDetails, species, isLead, teraType, role);
} else {
item = this.getItem(ability, types, moves, counter, teamDetails, species, isLead, teraType, role);
}
}
// Get level
const level = this.getLevel(species, isDoubles);
// Prepare optimal HP
const srImmunity = ability === 'Magic Guard' || item === 'Heavy-Duty Boots';
let srWeakness = srImmunity ? 0 : this.dex.getEffectiveness('Rock', species);
// Crash damage move users want an odd HP to survive two misses
if (['axekick', 'highjumpkick', 'jumpkick', 'supercellslam'].some(m => moves.has(m))) srWeakness = 2;
while (evs.hp > 1) {
const hp = Math.floor(Math.floor(2 * species.baseStats.hp + ivs.hp + Math.floor(evs.hp / 4) + 100) * level / 100 + 10);
if ((moves.has('substitute') && ['Sitrus Berry', 'Salac Berry'].includes(item)) || species.id === 'minior') {
// Two Substitutes should activate Sitrus Berry. Two switch-ins to Stealth Rock should activate Shields Down on Minior.
if (hp % 4 === 0) break;
} else if (
(moves.has('bellydrum') || moves.has('filletaway') || moves.has('shedtail')) &&
(item === 'Sitrus Berry' || ability === 'Gluttony')
) {
// Belly Drum should activate Sitrus Berry
if (hp % 2 === 0) break;
} else if (moves.has('substitute') && moves.has('endeavor')) {
// Luvdisc should be able to Substitute down to very low HP
if (hp % 4 > 0) break;
} else {
// Maximize number of Stealth Rock switch-ins in singles
if (isDoubles) break;
if (srWeakness <= 0 || ability === 'Regenerator' || ['Leftovers', 'Life Orb'].includes(item)) break;
if (item !== 'Sitrus Berry' && hp % (4 / srWeakness) > 0) break;
// Minimise number of Stealth Rock switch-ins to activate Sitrus Berry
if (item === 'Sitrus Berry' && hp % (4 / srWeakness) === 0) break;
}
evs.hp -= 4;
}
// Minimize confusion damage
const noAttackStatMoves = [...moves].every(m => {
const move = this.dex.moves.get(m);
if (move.damageCallback || move.damage) return true;
if (move.id === 'shellsidearm') return false;
// Physical Tera Blast
if (
move.id === 'terablast' && (species.id === 'porygon2' || ['Contrary', 'Defiant'].includes(ability) ||
moves.has('shiftgear') || species.baseStats.atk > species.baseStats.spa)
) return false;
return move.category !== 'Physical' || move.id === 'bodypress' || move.id === 'foulplay';
});
if (noAttackStatMoves && !moves.has('transform') && this.format.mod !== 'partnersincrime') {
evs.atk = 0;
ivs.atk = 0;
}
if (moves.has('gyroball') || moves.has('trickroom')) {
evs.spe = 0;
ivs.spe = 0;
}
// Enforce Tera Type after all set generation is done to prevent infinite generation
if (this.forceTeraType) teraType = this.forceTeraType;
// shuffle moves to add more randomness to camomons
const shuffledMoves = Array.from(moves);
this.prng.shuffle(shuffledMoves);
return {
name: species.baseSpecies,
species: forme,
gender: species.baseSpecies === 'Greninja' ? 'M' : species.gender,
shiny: this.randomChance(1, 1024),
level,
moves: shuffledMoves,
ability,
evs,
ivs,
item,
teraType,
role,
};
}
getPokemonPool(
type: string,
pokemonToExclude: RandomTeamsTypes.RandomSet[] = [],
isMonotype = false,
pokemonList: string[]
): [{ [k: string]: string[] }, string[]] {
const exclude = pokemonToExclude.map(p => toID(p.species));
const pokemonPool: { [k: string]: string[] } = {};
const baseSpeciesPool = [];
for (const pokemon of pokemonList) {
let species = this.dex.species.get(pokemon);
if (exclude.includes(species.id)) continue;
if (isMonotype) {
if (!species.types.includes(type)) continue;
if (typeof species.battleOnly === 'string') {
species = this.dex.species.get(species.battleOnly);
if (!species.types.includes(type)) continue;
}
}
if (species.baseSpecies in pokemonPool) {
pokemonPool[species.baseSpecies].push(pokemon);
} else {
pokemonPool[species.baseSpecies] = [pokemon];
}
}
// Include base species 1x if 1-3 formes, 2x if 4-6 formes, 3x if 7+ formes
for (const baseSpecies of Object.keys(pokemonPool)) {
// Squawkabilly has 4 formes, but only 2 functionally different formes, so only include it 1x
const weight = (baseSpecies === 'Squawkabilly') ? 1 : Math.min(Math.ceil(pokemonPool[baseSpecies].length / 3), 3);
for (let i = 0; i < weight; i++) baseSpeciesPool.push(baseSpecies);
}
return [pokemonPool, baseSpeciesPool];
}
randomSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./sets.json');
randomDoublesSets: { [species: string]: RandomTeamsTypes.RandomSpeciesData } = require('./doubles-sets.json');
randomTeam() {
this.enforceNoDirectCustomBanlistChanges();
const seed = this.prng.getSeed();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const pokemon: RandomTeamsTypes.RandomSet[] = [];
// For Monotype
const isMonotype = !!this.forceMonotype || ruleTable.has('sametypeclause');
const isDoubles = this.format.gameType !== 'singles';
const typePool = this.dex.types.names().filter(name => name !== "Stellar");
const type = this.forceMonotype || this.sample(typePool);
// PotD stuff
const usePotD = global.Config && Config.potd && ruleTable.has('potd');
const potd = usePotD ? this.dex.species.get(Config.potd) : null;
const baseFormes: { [k: string]: number } = {};
const typeCount: { [k: string]: number } = {};
const typeComboCount: { [k: string]: number } = {};
const typeWeaknesses: { [k: string]: number } = {};
const typeDoubleWeaknesses: { [k: string]: number } = {};
const teamDetails: RandomTeamsTypes.TeamDetails = {};
let numMaxLevelPokemon = 0;
const pokemonList = isDoubles ? Object.keys(this.randomDoublesSets) : Object.keys(this.randomSets);
const [pokemonPool, baseSpeciesPool] = this.getPokemonPool(type, pokemon, isMonotype, pokemonList);
let leadsRemaining = this.format.gameType === 'doubles' ? 2 : 1;
while (baseSpeciesPool.length && pokemon.length < this.maxTeamSize) {
const baseSpecies = this.sampleNoReplace(baseSpeciesPool);
let species = this.dex.species.get(this.sample(pokemonPool[baseSpecies]));
if (!species.exists) continue;
// Limit to one of each species (Species Clause)
if (baseFormes[species.baseSpecies]) continue;
// Treat Ogerpon formes and Terapagos like the Tera Blast user role; reject if team has one already
if (['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id) && teamDetails.teraBlast) continue;
// Illusion shouldn't be on the last slot
if (species.baseSpecies === 'Zoroark' && pokemon.length >= (this.maxTeamSize - 1)) continue;
const types = species.types;
const typeCombo = types.slice().sort().join();
const weakToFreezeDry = (
this.dex.getEffectiveness('Ice', species) > 0 ||
(this.dex.getEffectiveness('Ice', species) > -2 && types.includes('Water'))
);
// Dynamically scale limits for different team sizes. The default and minimum value is 1.
const limitFactor = Math.round(this.maxTeamSize / 6) || 1;
if (!isMonotype && !this.forceMonotype) {
let skip = false;
// Limit two of any type
for (const typeName of types) {
if (typeCount[typeName] >= 2 * limitFactor) {
skip = true;
break;
}
}
if (skip) continue;
// Limit three weak to any type, and one double weak to any type
for (const typeName of this.dex.types.names()) {
// it's weak to the type
if (this.dex.getEffectiveness(typeName, species) > 0) {
if (!typeWeaknesses[typeName]) typeWeaknesses[typeName] = 0;
if (typeWeaknesses[typeName] >= 3 * limitFactor) {
skip = true;
break;
}
}
if (this.dex.getEffectiveness(typeName, species) > 1) {
if (!typeDoubleWeaknesses[typeName]) typeDoubleWeaknesses[typeName] = 0;
if (typeDoubleWeaknesses[typeName] >= limitFactor) {
skip = true;
break;
}
}
}
if (skip) continue;
// Count Dry Skin/Fluffy as Fire weaknesses
if (
this.dex.getEffectiveness('Fire', species) === 0 &&
Object.values(species.abilities).filter(a => ['Dry Skin', 'Fluffy'].includes(a)).length
) {
if (!typeWeaknesses['Fire']) typeWeaknesses['Fire'] = 0;
if (typeWeaknesses['Fire'] >= 3 * limitFactor) continue;
}
// Limit four weak to Freeze-Dry
if (weakToFreezeDry) {
if (!typeWeaknesses['Freeze-Dry']) typeWeaknesses['Freeze-Dry'] = 0;
if (typeWeaknesses['Freeze-Dry'] >= 4 * limitFactor) continue;
}
// Limit one level 100 Pokemon
if (!this.adjustLevel && (this.getLevel(species, isDoubles) === 100) && numMaxLevelPokemon >= limitFactor) {
continue;
}
}
// Limit three of any type combination in Monotype
if (!this.forceMonotype && isMonotype && (typeComboCount[typeCombo] >= 3 * limitFactor)) continue;
// The Pokemon of the Day
if (potd?.exists && (pokemon.length === 1 || this.maxTeamSize === 1)) species = potd;
let set: RandomTeamsTypes.RandomSet;
if (leadsRemaining) {
if (
isDoubles && DOUBLES_NO_LEAD_POKEMON.includes(species.baseSpecies) ||
!isDoubles && NO_LEAD_POKEMON.includes(species.baseSpecies)
) {
if (pokemon.length + leadsRemaining === this.maxTeamSize) continue;
set = this.randomSet(species, teamDetails, false, isDoubles);
pokemon.push(set);
} else {
set = this.randomSet(species, teamDetails, true, isDoubles);
pokemon.unshift(set);
leadsRemaining--;
}
} else {
set = this.randomSet(species, teamDetails, false, isDoubles);
pokemon.push(set);
}
// Don't bother tracking details for the last Pokemon
if (pokemon.length === this.maxTeamSize) break;
// Now that our Pokemon has passed all checks, we can increment our counters
baseFormes[species.baseSpecies] = 1;
// Increment type counters
for (const typeName of types) {
if (typeName in typeCount) {
typeCount[typeName]++;
} else {
typeCount[typeName] = 1;
}
}
if (typeCombo in typeComboCount) {
typeComboCount[typeCombo]++;
} else {
typeComboCount[typeCombo] = 1;
}
// Increment weakness counter
for (const typeName of this.dex.types.names()) {
// it's weak to the type
if (this.dex.getEffectiveness(typeName, species) > 0) {
typeWeaknesses[typeName]++;
}
if (this.dex.getEffectiveness(typeName, species) > 1) {
typeDoubleWeaknesses[typeName]++;
}
}
// Count Dry Skin/Fluffy as Fire weaknesses
if (['Dry Skin', 'Fluffy'].includes(set.ability) && this.dex.getEffectiveness('Fire', species) === 0) {
typeWeaknesses['Fire']++;
}
if (weakToFreezeDry) typeWeaknesses['Freeze-Dry']++;
// Increment level 100 counter
if (set.level === 100) numMaxLevelPokemon++;
// Track what the team has
if (set.ability === 'Drizzle' || set.moves.includes('raindance')) teamDetails.rain = 1;
if (set.ability === 'Drought' || set.ability === 'Orichalcum Pulse' || set.moves.includes('sunnyday')) {
teamDetails.sun = 1;
}
if (set.ability === 'Sand Stream') teamDetails.sand = 1;
if (set.ability === 'Snow Warning' || set.moves.includes('snowscape') || set.moves.includes('chillyreception')) {
teamDetails.snow = 1;
}
if (set.moves.includes('healbell')) teamDetails.statusCure = 1;
if (set.moves.includes('spikes') || set.moves.includes('ceaselessedge')) {
teamDetails.spikes = (teamDetails.spikes || 0) + 1;
}
if (set.moves.includes('toxicspikes') || set.ability === 'Toxic Debris') teamDetails.toxicSpikes = 1;
if (set.moves.includes('stealthrock') || set.moves.includes('stoneaxe')) teamDetails.stealthRock = 1;
if (set.moves.includes('stickyweb')) teamDetails.stickyWeb = 1;
if (set.moves.includes('defog')) teamDetails.defog = 1;
if (set.moves.includes('rapidspin') || set.moves.includes('mortalspin')) teamDetails.rapidSpin = 1;
if (set.moves.includes('auroraveil') || (set.moves.includes('reflect') && set.moves.includes('lightscreen'))) {
teamDetails.screens = 1;
}
if (set.role === 'Tera Blast user' || ['ogerpon', 'ogerponhearthflame', 'terapagos'].includes(species.id)) {
teamDetails.teraBlast = 1;
}
}
if (pokemon.length < this.maxTeamSize && pokemon.length < 12) { // large teams sometimes cannot be built
throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`);
}
return pokemon;
}
randomCCTeam(): RandomTeamsTypes.RandomSet[] {
this.enforceNoDirectCustomBanlistChanges();
const dex = this.dex;
const team = [];
const natures = this.dex.natures.all();
const items = this.dex.items.all();
const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined, undefined, true);
for (let forme of randomN) {
let species = dex.species.get(forme);
if (species.isNonstandard) species = dex.species.get(species.baseSpecies);
// Random legal item
let item = '';
let isIllegalItem;
let isBadItem;
if (this.gen >= 2) {
do {
item = this.sample(items).name;
isIllegalItem = this.dex.items.get(item).gen > this.gen || this.dex.items.get(item).isNonstandard;
isBadItem = item.startsWith("TR") || this.dex.items.get(item).isPokeball;
} while (isIllegalItem || (isBadItem && this.randomChance(19, 20)));
}
// Make sure forme is legal
if (species.battleOnly) {
if (typeof species.battleOnly === 'string') {
species = dex.species.get(species.battleOnly);
} else {
species = dex.species.get(this.sample(species.battleOnly));
}
forme = species.name;
} else if (species.requiredItems && !species.requiredItems.some(req => toID(req) === item)) {
if (!species.changesFrom) throw new Error(`${species.name} needs a changesFrom value`);
species = dex.species.get(species.changesFrom);
forme = species.name;
}
// Make sure that a base forme does not hold any forme-modifier items.
let itemData = this.dex.items.get(item);
if (itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies) {
do {
itemData = this.sample(items);
item = itemData.name;
} while (
itemData.gen > this.gen ||
itemData.isNonstandard ||
(itemData.forcedForme && forme === this.dex.species.get(itemData.forcedForme).baseSpecies)
);
}
// Random legal ability
const abilities = Object.values(species.abilities).filter(a => this.dex.abilities.get(a).gen <= this.gen);
const ability: string = this.gen <= 2 ? 'No Ability' : this.sample(abilities);
// Four random unique moves from the movepool
let pool = ['struggle'];
if (forme === 'Smeargle') {
pool = this.dex.moves.all()
.filter(move => !(move.isNonstandard || move.isZ || move.isMax || move.realMove))
.map(m => m.id);
} else {
pool = [...this.dex.species.getMovePool(species.id)];
}
const moves = this.multipleSamplesNoReplace(pool, this.maxMoveCount);
// Random EVs
const evs: StatsTable = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
const s: StatID[] = ["hp", "atk", "def", "spa", "spd", "spe"];
let evpool = 510;
do {
const x = this.sample(s);
const y = this.random(Math.min(256 - evs[x], evpool + 1));
evs[x] += y;
evpool -= y;
} while (evpool > 0);
// Random IVs
const ivs = {
hp: this.random(32),
atk: this.random(32),
def: this.random(32),
spa: this.random(32),
spd: this.random(32),
spe: this.random(32),
};
// Random nature
const nature = this.sample(natures).name;
// Level balance--calculate directly from stats rather than using some silly lookup table
const mbstmin = 1307; // Sunkern has the lowest modified base stat total, and that total is 807
let stats = species.baseStats;
// If Wishiwashi, use the school-forme's much higher stats
if (species.baseSpecies === 'Wishiwashi') stats = Dex.species.get('wishiwashischool').baseStats;
// If Terapagos, use Terastal-forme's stats
if (species.baseSpecies === 'Terapagos') stats = Dex.species.get('terapagosterastal').baseStats;
// Modified base stat total assumes 31 IVs, 85 EVs in every stat
let mbst = (stats["hp"] * 2 + 31 + 21 + 100) + 10;
mbst += (stats["atk"] * 2 + 31 + 21 + 100) + 5;
mbst += (stats["def"] * 2 + 31 + 21 + 100) + 5;
mbst += (stats["spa"] * 2 + 31 + 21 + 100) + 5;
mbst += (stats["spd"] * 2 + 31 + 21 + 100) + 5;
mbst += (stats["spe"] * 2 + 31 + 21 + 100) + 5;
let level;
if (this.adjustLevel) {
level = this.adjustLevel;
} else {
level = Math.floor(100 * mbstmin / mbst); // Initial level guess will underestimate
while (level < 100) {
mbst = Math.floor((stats["hp"] * 2 + 31 + 21 + 100) * level / 100 + 10);
// Since damage is roughly proportional to level
mbst += Math.floor(((stats["atk"] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100);
mbst += Math.floor((stats["def"] * 2 + 31 + 21 + 100) * level / 100 + 5);
mbst += Math.floor(((stats["spa"] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100);
mbst += Math.floor((stats["spd"] * 2 + 31 + 21 + 100) * level / 100 + 5);
mbst += Math.floor((stats["spe"] * 2 + 31 + 21 + 100) * level / 100 + 5);
if (mbst >= mbstmin) break;
level++;
}
}
// Random happiness
const happiness = this.random(256);
// Random shininess
const shiny = this.randomChance(1, 1024);
const set: RandomTeamsTypes.RandomSet = {
name: species.baseSpecies,
species: species.name,
gender: species.gender,
item,
ability,
moves,
evs,
ivs,
nature,
level,
happiness,
shiny,
};
if (this.gen === 9) {
// Tera type
if (this.forceTeraType) {
set.teraType = this.forceTeraType;
} else {
set.teraType = this.sample(this.dex.types.names());
}
}
team.push(set);
}
return team;
}
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;
let pool: number[] = [];
let speciesPool: Species[] = [];
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;
if (requireMoves) {
const hasMovesInCurrentGen = this.dex.species.getMovePool(species.id).size;
if (!hasMovesInCurrentGen) continue;
}
if (requiredType && !species.types.includes(requiredType)) continue;
if (minSourceGen && species.gen < minSourceGen) continue;
const num = species.num;
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');
// Assume tierSpecies does not differ from species here (mega formes can be used without their stone, etc)
for (const species of this.dex.species.all()) {
if (requiredType && !species.types.includes(requiredType)) continue;
let banReason = ruleTable.check('pokemon:' + species.id);
if (banReason) continue;
if (banReason !== '') {
if (species.isMega && ruleTable.check('pokemontag:mega')) continue;
banReason = ruleTable.check('basepokemon:' + toID(species.baseSpecies));
if (banReason) continue;
if (banReason !== '' || this.dex.species.get(species.baseSpecies).isNonstandard !== species.isNonstandard) {
const nonexistentCheck = Tags.nonexistent.genericFilter!(species) && nonexistentBanReason;
let tagWhitelisted = false;
let tagBlacklisted = false;
for (const ruleid of ruleTable.tagRules) {
if (ruleid.startsWith('*')) continue;
const tagid = ruleid.slice(12) as ID;
const tag = Tags[tagid];
if ((tag.speciesFilter || tag.genericFilter)!(species)) {
const existenceTag = EXISTENCE_TAG.includes(tagid);
if (ruleid.startsWith('+')) {
if (!existenceTag && nonexistentCheck) continue;
tagWhitelisted = true;
break;
}
tagBlacklisted = true;
break;
}
}
if (tagBlacklisted) continue;
if (!tagWhitelisted) {
if (ruleTable.check('pokemontag:allpokemon')) continue;
}
}
}
speciesPool.push(species);
const num = species.num;
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++) {
const num = this.sampleNoReplace(pool);
hasDexNumber[num] = i;
}
const formes: string[][] = [];
for (const species of speciesPool) {
if (!(species.num in hasDexNumber)) continue;
if (isNotCustom && (species.gen > this.gen ||
(species.isNonstandard && species.isNonstandard !== 'Unobtainable'))) continue;
if (requiredType && !species.types.includes(requiredType)) continue;
if (!formes[hasDexNumber[species.num]]) formes[hasDexNumber[species.num]] = [];
formes[hasDexNumber[species.num]].push(species.name);
}
if (formes.length < n) {
throw new Error(`Legal Pokemon forme count insufficient to support Max Team Size: (${formes.length} / ${n}).`);
}
const nPokemon = [];
for (let i = 0; i < n; i++) {
if (!formes[i].length) {
throw new Error(`Invalid pokemon gen ${this.gen}: ${JSON.stringify(formes)} numbers ${JSON.stringify(hasDexNumber)}`);
}
nPokemon.push(this.sample(formes[i]));
}
return nPokemon;
}
randomHCTeam(): PokemonSet[] {
const hasCustomBans = this.hasDirectCustomBanlistChanges();
const ruleTable = this.dex.formats.getRuleTable(this.format);
const hasNonexistentBan = hasCustomBans && ruleTable.check('nonexistent');
const hasNonexistentWhitelist = hasCustomBans && (hasNonexistentBan === '');
if (hasCustomBans) {
this.enforceNoDirectComplexBans();
}
// Item Pool
const doItemsExist = this.gen > 1;
let itemPool: Item[] = [];
if (doItemsExist) {
if (!hasCustomBans) {
itemPool = [...this.dex.items.all()].filter(item => (item.gen <= this.gen && !item.isNonstandard));
} else {
const hasAllItemsBan = ruleTable.check('pokemontag:allitems');
for (const item of this.dex.items.all()) {
let banReason = ruleTable.check('item:' + item.id);
if (banReason) continue;
if (banReason !== '' && item.id) {
if (hasAllItemsBan) continue;
if (item.isNonstandard) {
banReason = ruleTable.check('pokemontag:' + toID(item.isNonstandard));
if (banReason) continue;
if (banReason !== '' && item.isNonstandard !== 'Unobtainable') {
if (hasNonexistentBan) continue;
if (!hasNonexistentWhitelist) continue;
}
}
}
itemPool.push(item);
}
if (ruleTable.check('item:noitem')) {
this.enforceCustomPoolSizeNoComplexBans('item', itemPool, this.maxTeamSize, 'Max Team Size');
}
}
}
// Ability Pool
const doAbilitiesExist = (this.gen > 2) && (this.dex.currentMod !== 'gen7letsgo');
let abilityPool: Ability[] = [];
if (doAbilitiesExist) {
if (!hasCustomBans) {
abilityPool = [...this.dex.abilities.all()].filter(ability => (ability.gen <= this.gen && !ability.isNonstandard));
} else {
const hasAllAbilitiesBan = ruleTable.check('pokemontag:allabilities');
for (const ability of this.dex.abilities.all()) {
let banReason = ruleTable.check('ability:' + ability.id);
if (banReason) continue;
if (banReason !== '') {
if (hasAllAbilitiesBan) continue;
if (ability.isNonstandard) {
banReason = ruleTable.check('pokemontag:' + toID(ability.isNonstandard));
if (banReason) continue;
if (banReason !== '') {
if (hasNonexistentBan) continue;
if (!hasNonexistentWhitelist) continue;
}
}
}
abilityPool.push(ability);
}
if (ruleTable.check('ability:noability')) {
this.enforceCustomPoolSizeNoComplexBans('ability', abilityPool, this.maxTeamSize, 'Max Team Size');
}
}
}
// Move Pool
const setMoveCount = ruleTable.maxMoveCount;
let movePool: Move[] = [];
if (!hasCustomBans) {
movePool = [...this.dex.moves.all()].filter(move =>
(move.gen <= this.gen && !move.isNonstandard));
} else {
const hasAllMovesBan = ruleTable.check('pokemontag:allmoves');
for (const move of this.dex.moves.all()) {
let banReason = ruleTable.check('move:' + move.id);
if (banReason) continue;
if (banReason !== '') {
if (hasAllMovesBan) continue;
if (move.isNonstandard) {
banReason = ruleTable.check('pokemontag:' + toID(move.isNonstandard));
if (banReason) continue;
if (banReason !== '' && move.isNonstandard !== 'Unobtainable') {
if (hasNonexistentBan) continue;
if (!hasNonexistentWhitelist) continue;
}
}
}
movePool.push(move);
}
this.enforceCustomPoolSizeNoComplexBans('move', movePool, this.maxTeamSize * setMoveCount, 'Max Team Size * Max Move Count');
}
// Nature Pool
const doNaturesExist = this.gen > 2;
let naturePool: Nature[] = [];
if (doNaturesExist) {
if (!hasCustomBans) {
naturePool = [...this.dex.natures.all()];
} else {
const hasAllNaturesBan = ruleTable.check('pokemontag:allnatures');
for (const nature of this.dex.natures.all()) {
let banReason = ruleTable.check('nature:' + nature.id);
if (banReason) continue;
if (banReason !== '' && nature.id) {
if (hasAllNaturesBan) continue;
if (nature.isNonstandard) {
banReason = ruleTable.check('pokemontag:' + toID(nature.isNonstandard));
if (banReason) continue;
if (banReason !== '' && nature.isNonstandard !== 'Unobtainable') {
if (hasNonexistentBan) continue;
if (!hasNonexistentWhitelist) continue;
}
}
}
naturePool.push(nature);
}
// There is no 'nature:nonature' rule so do not constrain pool size
}
}
const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype, undefined,
hasCustomBans ? ruleTable : undefined);
const team = [];
for (const forme of randomN) {
// Choose forme
const species = this.dex.species.get(forme);
// Random unique item
let item = '';
let itemData;
let isBadItem;
if (doItemsExist) {
// We discard TRs and Balls with 95% probability because of their otherwise overwhelming presence
do {
itemData = this.sampleNoReplace(itemPool);
item = itemData?.name;
isBadItem = item.startsWith("TR") || itemData.isPokeball;
} while (isBadItem && this.randomChance(19, 20) && itemPool.length > this.maxTeamSize);
}
// Random unique ability
let ability = 'No Ability';
let abilityData;
if (doAbilitiesExist) {
abilityData = this.sampleNoReplace(abilityPool);
ability = abilityData?.name;
}
// Random unique moves
const m = [];
do {
const move = this.sampleNoReplace(movePool);
m.push(move.id);
} while (m.length < setMoveCount);
// Random EVs
const evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
if (this.gen === 6) {
let evpool = 510;
do {
const x = this.sample(Dex.stats.ids());
const y = this.random(Math.min(256 - evs[x], evpool + 1));
evs[x] += y;
evpool -= y;
} while (evpool > 0);
} else {
for (const x of Dex.stats.ids()) {
evs[x] = this.random(256);
}
}
// Random IVs
const ivs: StatsTable = {
hp: this.random(32),
atk: this.random(32),
def: this.random(32),
spa: this.random(32),
spd: this.random(32),
spe: this.random(32),
};
// Random nature
let nature = '';
if (doNaturesExist && (naturePool.length > 0)) {
nature = this.sample(naturePool).name;
}
// Level balance
const mbstmin = 1307;
const stats = species.baseStats;
let mbst = (stats['hp'] * 2 + 31 + 21 + 100) + 10;
mbst += (stats['atk'] * 2 + 31 + 21 + 100) + 5;
mbst += (stats['def'] * 2 + 31 + 21 + 100) + 5;
mbst += (stats['spa'] * 2 + 31 + 21 + 100) + 5;
mbst += (stats['spd'] * 2 + 31 + 21 + 100) + 5;
mbst += (stats['spe'] * 2 + 31 + 21 + 100) + 5;
let level;
if (this.adjustLevel) {
level = this.adjustLevel;
} else {
level = Math.floor(100 * mbstmin / mbst);
while (level < 100) {
mbst = Math.floor((stats['hp'] * 2 + 31 + 21 + 100) * level / 100 + 10);
mbst += Math.floor(((stats['atk'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100);
mbst += Math.floor((stats['def'] * 2 + 31 + 21 + 100) * level / 100 + 5);
mbst += Math.floor(((stats['spa'] * 2 + 31 + 21 + 100) * level / 100 + 5) * level / 100);
mbst += Math.floor((stats['spd'] * 2 + 31 + 21 + 100) * level / 100 + 5);
mbst += Math.floor((stats['spe'] * 2 + 31 + 21 + 100) * level / 100 + 5);
if (mbst >= mbstmin) break;
level++;
}
}
// Random happiness
const happiness = this.random(256);
// Random shininess
const shiny = this.randomChance(1, 1024);
const set: PokemonSet = {
name: species.baseSpecies,
species: species.name,
gender: species.gender,
item,
ability,
moves: m,
evs,
ivs,
nature,
level,
happiness,
shiny,
};
if (this.gen === 9) {
// Random Tera type
if (this.forceTeraType) {
set.teraType = this.forceTeraType;
} else {
set.teraType = this.sample(this.dex.types.names());
}
}
team.push(set);
}
return team;
}
randomFactorySets: { [format: string]: { [species: string]: BattleFactorySpecies } } = require('./factory-sets.json');
randomFactorySet(
species: Species, teamData: RandomTeamsTypes.FactoryTeamDetails, tier: string
): RandomTeamsTypes.RandomFactorySet | null {
const id = toID(species.name);
const setList = this.randomFactorySets[tier][id].sets;
const itemsLimited = ['choicespecs', 'choiceband', 'choicescarf'];
const movesLimited: { [k: string]: string } = {
stealthrock: 'stealthRock',
stoneaxe: 'stealthRock',
spikes: 'spikes',
ceaselessedge: 'spikes',
toxicspikes: 'toxicSpikes',
rapidspin: 'hazardClear',
defog: 'hazardClear',
};
const abilitiesLimited: { [k: string]: string } = {
toxicdebris: 'toxicSpikes',
};
// Build a pool of eligible sets, given the team partners
// Also keep track of moves and items limited to one per team
const effectivePool: {
set: BattleFactorySet, moves?: string[], item?: string,
}[] = [];
for (const set of setList) {
let reject = false;
// limit to 1 dedicated tera user per team
if (set.wantsTera && teamData.wantsTeraCount) {
continue;
}
// reject disallowed items, specifically a second of any given choice item
const allowedItems: string[] = [];
for (const itemString of set.item) {
const itemId = toID(itemString);
if (itemsLimited.includes(itemId) && teamData.has[itemId]) continue;
allowedItems.push(itemString);
}
if (!allowedItems.length) continue;
const item = this.sample(allowedItems);
const abilityId = toID(this.sample(set.ability));
if (abilitiesLimited[abilityId] && teamData.has[abilitiesLimited[abilityId]]) continue;
const moves: string[] = [];
for (const move of set.moves) {
const allowedMoves: string[] = [];
for (const m of move) {
const moveId = toID(m);
if (movesLimited[moveId] && teamData.has[movesLimited[moveId]]) continue;
allowedMoves.push(m);
}
if (!allowedMoves.length) {
reject = true;
break;
}
moves.push(this.sample(allowedMoves));
}
if (reject) continue;
effectivePool.push({ set, moves, item });
}
if (!effectivePool.length) {
if (!teamData.forceResult) return null;
for (const set of setList) {
effectivePool.push({ set });
}
}
// Sets have individual weight, choose one with weighted random selection
let setData = this.sample(effectivePool); // Init with unweighted random set as fallback
const total = effectivePool.reduce((a, b) => a + b.set.weight, 0);
const setRand = this.random(total);
let cur = 0;
for (const set of effectivePool) {
cur += set.set.weight;
if (cur > setRand) {
setData = set; // Bingo!
break;
}
}
const moves = [];
for (const [i, moveSlot] of setData.set.moves.entries()) {
moves.push(setData.moves ? setData.moves[i] : this.sample(moveSlot));
}
const item = setData.item || this.sample(setData.set.item);
return {
name: species.baseSpecies,
species: (typeof species.battleOnly === 'string') ? species.battleOnly : species.name,
teraType: this.sample(setData.set.teraType),
gender: setData.set.gender || species.gender || (tier === 'OU' ? 'F' : ''), // F for Cute Charm Enamorus
item,
ability: this.sample(setData.set.ability),
shiny: setData.set.shiny || this.randomChance(1, 1024),
level: this.adjustLevel || (tier === "LC" ? 5 : 100),
happiness: 255,
evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, ...setData.set.evs },
ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, ...setData.set.ivs },
nature: this.sample(setData.set.nature) || "Serious",
moves,
wantsTera: setData.set.wantsTera,
};
}
randomFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
this.enforceNoDirectCustomBanlistChanges();
const forceResult = depth >= 12;
if (!this.factoryTier) {
// this.factoryTier = this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU', 'LC']);
this.factoryTier = this.sample(['Uber', 'OU', 'UU', 'RU', 'NU', 'PU']);
}
const tierValues: { [k: string]: number } = {
Uber: 5,
OU: 4, UUBL: 4,
UU: 3, RUBL: 3,
RU: 2, NUBL: 2,
NU: 1, PUBL: 1,
PU: 0,
};
const pokemon = [];
const pokemonPool = Object.keys(this.randomFactorySets[this.factoryTier]);
const teamData: TeamData = {
typeCount: {},
typeComboCount: {},
baseFormes: {},
has: {},
wantsTeraCount: 0,
forceResult,
weaknesses: {},
resistances: {},
};
const resistanceAbilities: { [k: string]: string[] } = {
dryskin: ['Water'], waterabsorb: ['Water'], stormdrain: ['Water'],
flashfire: ['Fire'], heatproof: ['Fire'], waterbubble: ['Fire'], wellbakedbody: ['Fire'],
lightningrod: ['Electric'], motordrive: ['Electric'], voltabsorb: ['Electric'],
sapsipper: ['Grass'],
thickfat: ['Ice', 'Fire'],
eartheater: ['Ground'], levitate: ['Ground'],
};
const movesLimited: { [k: string]: string } = {
stealthrock: 'stealthRock',
stoneaxe: 'stealthRock',
spikes: 'spikes',
ceaselessedge: 'spikes',
toxicspikes: 'toxicSpikes',
rapidspin: 'hazardClear',
defog: 'hazardClear',
};
const abilitiesLimited: { [k: string]: string } = {
toxicdebris: 'toxicSpikes',
};
const limitFactor = Math.ceil(this.maxTeamSize / 6);
/**
* Weighted random shuffle
* Uses the fact that for two uniform variables x1 and x2, x1^(1/w1) is larger than x2^(1/w2)
* with probability equal to w1/(w1+w2), which is what we want. See e.g. here https://arxiv.org/pdf/1012.0256.pdf,
* original paper is behind a paywall.
*/
const shuffledSpecies = [];
for (const speciesName of pokemonPool) {
const sortObject = {
speciesName,
score: this.prng.random() ** (1 / this.randomFactorySets[this.factoryTier][speciesName].weight),
};
shuffledSpecies.push(sortObject);
}
shuffledSpecies.sort((a, b) => a.score - b.score);
while (shuffledSpecies.length && pokemon.length < this.maxTeamSize) {
// repeated popping from weighted shuffle is equivalent to repeated weighted sampling without replacement
const species = this.dex.species.get(shuffledSpecies.pop()!.speciesName);
if (!species.exists) continue;
// Lessen the need of deleting sets of Pokemon after tier shifts
if (
this.factoryTier in tierValues && species.tier in tierValues &&
tierValues[species.tier] > tierValues[this.factoryTier]
) continue;
if (this.forceMonotype && !species.types.includes(this.forceMonotype)) continue;
// Limit to one of each species (Species Clause)
if (teamData.baseFormes[species.baseSpecies]) continue;
// Limit 2 of any type (most of the time)
const types = species.types;
let skip = false;
if (!this.forceMonotype) {
for (const type of types) {
if (teamData.typeCount[type] >= 2 * limitFactor && this.randomChance(4, 5)) {
skip = true;
break;
}
}
}
if (skip) continue;
if (!teamData.forceResult && !this.forceMonotype) {
// Limit 3 of any weakness
for (const typeName of this.dex.types.names()) {
// it's weak to the type
if (this.dex.getEffectiveness(typeName, species) > 0 && this.dex.getImmunity(typeName, types)) {
if (teamData.weaknesses[typeName] >= 3 * limitFactor) {
skip = true;
break;
}
}
}
}
if (skip) continue;
const set = this.randomFactorySet(species, teamData, this.factoryTier);
if (!set) continue;
// Limit 1 of any type combination
let typeCombo = types.slice().sort().join();
if (set.ability === "Drought" || set.ability === "Drizzle") {
// Drought and Drizzle don't count towards the type combo limit
typeCombo = set.ability;
}
if (!this.forceMonotype && teamData.typeComboCount[typeCombo] >= limitFactor) continue;
// Okay, the set passes, add it to our team
pokemon.push(set);
// Now that our Pokemon has passed all checks, we can update team data:
for (const type of types) {
if (type in teamData.typeCount) {
teamData.typeCount[type]++;
} else {
teamData.typeCount[type] = 1;
}
}
if (typeCombo in teamData.typeComboCount) {
teamData.typeComboCount[typeCombo]++;
} else {
teamData.typeComboCount[typeCombo] = 1;
}
teamData.baseFormes[species.baseSpecies] = 1;
teamData.has[toID(set.item)] = 1;
if (set.wantsTera) {
if (!teamData.wantsTeraCount) teamData.wantsTeraCount = 0;
teamData.wantsTeraCount++;
}
for (const move of set.moves) {
const moveId = toID(move);
if (movesLimited[moveId]) {
teamData.has[movesLimited[moveId]] = 1;
}
}
const ability = this.dex.abilities.get(set.ability);
if (abilitiesLimited[ability.id]) {
teamData.has[abilitiesLimited[ability.id]] = 1;
}
for (const typeName of this.dex.types.names()) {
const typeMod = this.dex.getEffectiveness(typeName, types);
// Track resistances because we will require it for triple weaknesses
if (
typeMod < 0 ||
resistanceAbilities[ability.id]?.includes(typeName) ||
!this.dex.getImmunity(typeName, types)
) {
// We don't care about the number of resistances, so just set to 1
teamData.resistances[typeName] = 1;
// Track weaknesses
} else if (typeMod > 0) {
teamData.weaknesses[typeName] = (teamData.weaknesses[typeName] || 0) + 1;
}
}
}
if (!teamData.forceResult && pokemon.length < this.maxTeamSize) return this.randomFactoryTeam(side, ++depth);
// Quality control we cannot afford for monotype
if (!teamData.forceResult && !this.forceMonotype) {
for (const type in teamData.weaknesses) {
// We reject if our team is triple weak to any type without having a resist
if (teamData.resistances[type]) continue;
if (teamData.weaknesses[type] >= 3 * limitFactor) return this.randomFactoryTeam(side, ++depth);
}
// Try to force Stealth Rock on non-Uber teams
if (!teamData.has['stealthRock'] && this.factoryTier !== 'Uber') return this.randomFactoryTeam(side, ++depth);
}
return pokemon;
}
randomBSSFactorySets: AnyObject = require("./bss-factory-sets.json");
randomBSSFactorySet(
species: Species, teamData: RandomTeamsTypes.FactoryTeamDetails
): RandomTeamsTypes.RandomFactorySet | null {
const id = toID(species.name);
const setList = this.randomBSSFactorySets[id].sets;
const movesMax: { [k: string]: number } = {
batonpass: 1,
stealthrock: 1,
toxicspikes: 1,
trickroom: 1,
auroraveil: 1,
};
const weatherAbilities = ['drizzle', 'drought', 'snowwarning', 'sandstream'];
const terrainAbilities: { [k: string]: string } = {
electricsurge: "electric",
psychicsurge: "psychic",
grassysurge: "grassy",
seedsower: "grassy",
mistysurge: "misty",
};
const terrainItemsRequire: { [k: string]: string } = {
electricseed: "electric",
psychicseed: "psychic",
grassyseed: "grassy",
mistyseed: "misty",
};
const maxWantsTera = 2;
// Build a pool of eligible sets, given the team partners
// Also keep track of sets with moves the team requires
const effectivePool: {
set: BSSFactorySet, moveVariants?: number[], itemVariants?: number, abilityVariants?: number,
}[] = [];
for (const curSet of setList) {
let reject = false;
// limit to 2 dedicated tera users per team
if (curSet.wantsTera && teamData.wantsTeraCount && teamData.wantsTeraCount >= maxWantsTera) {
continue;
}
// reject 2+ weather setters
if (teamData.weather && weatherAbilities.includes(curSet.ability)) {
continue;
}
if (terrainAbilities[curSet.ability]) {
if (!teamData.terrain) teamData.terrain = [];
teamData.terrain.push(terrainAbilities[curSet.ability]);
}
for (const item of curSet.item) {
if (terrainItemsRequire[item] && !teamData.terrain?.includes(terrainItemsRequire[item])) {
reject = true; // reject any sets with a seed item possible and no terrain setter to activate it
break;
}
}
const curSetMoveVariants = [];
for (const move of curSet.moves) {
const variantIndex = this.random(move.length);
const moveId = toID(move[variantIndex]);
if (movesMax[moveId] && teamData.has[moveId] >= movesMax[moveId]) {
reject = true;
break;
}
curSetMoveVariants.push(variantIndex);
}
if (reject) continue;
const set = { set: curSet, moveVariants: curSetMoveVariants };
effectivePool.push(set);
}
if (!effectivePool.length) {
if (!teamData.forceResult) return null;
for (const curSet of setList) {
effectivePool.push({ set: curSet });
}
}
// Sets have individual weight, choose one with weighted random selection
let setData = this.sample(effectivePool); // Init with unweighted random set as fallback
const total = effectivePool.reduce((a, b) => a + b.set.weight, 0);
const setRand = this.random(total);
let cur = 0;
for (const set of effectivePool) {
cur += set.set.weight;
if (cur > setRand) {
setData = set; // Bingo!
break;
}
}
const moves = [];
for (const [i, moveSlot] of setData.set.moves.entries()) {
moves.push(setData.moveVariants ? moveSlot[setData.moveVariants[i]] : this.sample(moveSlot));
}
return {
name: setData.set.species || species.baseSpecies,
species: setData.set.species,
teraType: (this.sampleIfArray(setData.set.teraType)),
gender: setData.set.gender || species.gender || (this.randomChance(1, 2) ? "M" : "F"),
item: this.sampleIfArray(setData.set.item) || "",
ability: this.sampleIfArray(setData.set.ability),
shiny: this.randomChance(1, 1024),
level: 50,
happiness: 255,
evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0, ...setData.set.evs },
ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31, ...setData.set.ivs },
nature: setData.set.nature || "Serious",
moves,
wantsTera: setData.set.wantsTera,
};
}
randomBSSFactoryTeam(side: PlayerOptions, depth = 0): RandomTeamsTypes.RandomFactorySet[] {
this.enforceNoDirectCustomBanlistChanges();
const forceResult = depth >= 4;
const pokemon = [];
const pokemonPool = Object.keys(this.randomBSSFactorySets);
const teamData: TeamData = {
typeCount: {},
typeComboCount: {},
baseFormes: {},
has: {},
wantsTeraCount: 0,
forceResult,
weaknesses: {},
resistances: {},
};
const weatherAbilitiesSet: { [k: string]: string } = {
drizzle: "raindance",
drought: "sunnyday",
snowwarning: "hail",
sandstream: "sandstorm",
};
const resistanceAbilities: { [k: string]: string[] } = {
waterabsorb: ["Water"],
flashfire: ["Fire"],
lightningrod: ["Electric"],
voltabsorb: ["Electric"],
thickfat: ["Ice", "Fire"],
levitate: ["Ground"],
};
const limitFactor = Math.ceil(this.maxTeamSize / 6);
/**
* Weighted random shuffle
* Uses the fact that for two uniform variables x1 and x2, x1^(1/w1) is larger than x2^(1/w2)
* with probability equal to w1/(w1+w2), which is what we want. See e.g. here https://arxiv.org/pdf/1012.0256.pdf,
* original paper is behind a paywall.
*/
const shuffledSpecies = [];
for (const speciesName of pokemonPool) {
const sortObject = {
speciesName,
score: this.prng.random() ** (1 / this.randomBSSFactorySets[speciesName].weight),
};
shuffledSpecies.push(sortObject);
}
shuffledSpecies.sort((a, b) => a.score - b.score);
while (shuffledSpecies.length && pokemon.length < this.maxTeamSize) {
// repeated popping from weighted shuffle is equivalent to repeated weighted sampling without replacement
const species = this.dex.species.get(shuffledSpecies.pop()!.speciesName);
if (!species.exists) continue;
if (this.forceMonotype && !species.types.includes(this.forceMonotype)) continue;
// Limit to one of each species (Species Clause)
if (teamData.baseFormes[species.baseSpecies]) continue;
// Limit 2 of any type (most of the time)
const types = species.types;
let skip = false;
if (!this.forceMonotype) {
for (const type of types) {
if (teamData.typeCount[type] >= 2 * limitFactor && this.randomChance(4, 5)) {
skip = true;
break;
}
}
}
if (skip) continue;
const set = this.randomBSSFactorySet(species, teamData);
if (!set) continue;
// Limit 1 of any type combination
let typeCombo = types.slice().sort().join();
if (set.ability === "Drought" || set.ability === "Drizzle") {
// Drought and Drizzle don't count towards the type combo limit
typeCombo = set.ability;
}
if (!this.forceMonotype && teamData.typeComboCount[typeCombo] >= limitFactor) continue;
const itemData = this.dex.items.get(set.item);
if (teamData.has[itemData.id]) continue; // Item Clause
// Okay, the set passes, add it to our team
pokemon.push(set);
// Now that our Pokemon has passed all checks, we can update team data:
for (const type of types) {
if (type in teamData.typeCount) {
teamData.typeCount[type]++;
} else {
teamData.typeCount[type] = 1;
}
}
if (typeCombo in teamData.typeComboCount) {
teamData.typeComboCount[typeCombo]++;
} else {
teamData.typeComboCount[typeCombo] = 1;
}
teamData.baseFormes[species.baseSpecies] = 1;
teamData.has[itemData.id] = 1;
if (set.wantsTera) {
if (!teamData.wantsTeraCount) teamData.wantsTeraCount = 0;
teamData.wantsTeraCount++;
}
const abilityState = this.dex.abilities.get(set.ability);
if (abilityState.id in weatherAbilitiesSet) {
teamData.weather = weatherAbilitiesSet[abilityState.id];
}
for (const move of set.moves) {
const moveId = toID(move);
if (moveId in teamData.has) {
teamData.has[moveId]++;
} else {
teamData.has[moveId] = 1;
}
}
for (const typeName of this.dex.types.names()) {
// Cover any major weakness (3+) with at least one resistance
if (teamData.resistances[typeName] >= 1) continue;
if (resistanceAbilities[abilityState.id]?.includes(typeName) || !this.dex.getImmunity(typeName, types)) {
// Heuristic: assume that Pokémon with these abilities don't have (too) negative typing.
teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1;
if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0;
continue;
}
const typeMod = this.dex.getEffectiveness(typeName, types);
if (typeMod < 0) {
teamData.resistances[typeName] = (teamData.resistances[typeName] || 0) + 1;
if (teamData.resistances[typeName] >= 1) teamData.weaknesses[typeName] = 0;
} else if (typeMod > 0) {
teamData.weaknesses[typeName] = (teamData.weaknesses[typeName] || 0) + 1;
}
}
}
if (!teamData.forceResult && pokemon.length < this.maxTeamSize) return this.randomBSSFactoryTeam(side, ++depth);
// Quality control we cannot afford for monotype
if (!teamData.forceResult && !this.forceMonotype) {
for (const type in teamData.weaknesses) {
if (teamData.weaknesses[type] >= 3 * limitFactor) return this.randomBSSFactoryTeam(side, ++depth);
}
}
return pokemon;
}
randomDraftFactoryMatchups: AnyObject = require("./draft-factory-matchups.json").matchups;
rdfMatchupIndex = -1;
rdfMatchupSide = -1;
randomDraftFactoryTeam(side: PlayerOptions): RandomTeamsTypes.RandomDraftFactorySet[] {
this.enforceNoDirectCustomBanlistChanges();
if (this.rdfMatchupIndex === -1) this.rdfMatchupIndex = this.random(0, this.randomDraftFactoryMatchups.length);
if (this.rdfMatchupSide === -1) this.rdfMatchupSide = this.random(0, 2);
const matchup = this.randomDraftFactoryMatchups[this.rdfMatchupIndex];
const team = Teams.unpack(matchup[this.rdfMatchupSide]);
if (!team) throw new Error(`Invalid team for draft factory matchup ${this.rdfMatchupIndex}`);
this.rdfMatchupSide = 1 - this.rdfMatchupSide;
return team.map(set => {
let species = this.dex.species.get(set.species);
if (species.battleOnly) {
if (typeof species.battleOnly !== 'string') {
throw new Error(`Invalid species ${species.name} for draft factory matchup ${this.rdfMatchupIndex} team ${this.rdfMatchupSide}`);
}
species = this.dex.species.get(species.battleOnly);
}
return {
name: species.baseSpecies,
species: species.name,
gender: set.gender,
moves: set.moves,
ability: set.ability,
evs: set.evs,
ivs: set.ivs,
item: set.item,
level: this.adjustLevel || set.level,
shiny: !!set.shiny,
nature: set.nature,
teraType: set.teraType,
teraCaptain: set.name === 'Tera Captain',
};
});
}
}
export default RandomTeams;