import RandomGen2Teams from '../gen2/random-teams'; import {Utils} from '../../../lib'; import {MoveCounter} from '../../random-teams'; interface HackmonsCupEntry { types: string[]; baseStats: StatsTable; } export class RandomGen1Teams extends RandomGen2Teams { // Challenge Cup or CC teams are basically fully random teams. randomCCTeam() { this.enforceNoDirectCustomBanlistChanges(); const team = []; const randomN = this.randomNPokemon(this.maxTeamSize, this.forceMonotype); for (const pokemon of randomN) { const species = this.dex.species.get(pokemon); const learnset = this.dex.species.getLearnset(species.id); // Level balance: calculate directly from stats rather than using some silly lookup table. const mbstmin = 1307; const stats = species.baseStats; // Modified base stat total assumes 15 DVs, 255 EVs in every stat let mbst = (stats["hp"] * 2 + 30 + 63 + 100) + 10; mbst += (stats["atk"] * 2 + 30 + 63 + 100) + 5; mbst += (stats["def"] * 2 + 30 + 63 + 100) + 5; mbst += (stats["spa"] * 2 + 30 + 63 + 100) + 5; mbst += (stats["spd"] * 2 + 30 + 63 + 100) + 5; mbst += (stats["spe"] * 2 + 30 + 63 + 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 + 30 + 63 + 100) * level / 100 + 10); // Since damage is roughly proportional to lvl mbst += Math.floor(((stats["atk"] * 2 + 30 + 63 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats["def"] * 2 + 30 + 63 + 100) * level / 100 + 5); mbst += Math.floor(((stats["spa"] * 2 + 30 + 63 + 100) * level / 100 + 5) * level / 100); mbst += Math.floor((stats["spd"] * 2 + 30 + 63 + 100) * level / 100 + 5); mbst += Math.floor((stats["spe"] * 2 + 30 + 63 + 100) * level / 100 + 5); if (mbst >= mbstmin) break; level++; } } // Random DVs. const ivs = { hp: 0, atk: this.random(16), def: this.random(16), spa: this.random(16), spd: 0, spe: this.random(16), }; ivs["hp"] = (ivs["atk"] % 2) * 16 + (ivs["def"] % 2) * 8 + (ivs["spe"] % 2) * 4 + (ivs["spa"] % 2) * 2; ivs["atk"] *= 2; ivs["def"] *= 2; ivs["spa"] *= 2; ivs["spd"] = ivs["spa"]; ivs["spe"] *= 2; // Maxed EVs. const evs = {hp: 255, atk: 255, def: 255, spa: 255, spd: 255, spe: 255}; // Four random unique moves from movepool. don't worry about "attacking" or "viable". // Since Gens 1 and 2 learnsets are shared, we need to weed out Gen 2 moves. const pool: string[] = []; if (learnset) { for (const move in learnset) { if (this.dex.moves.get(move).gen !== 1) continue; if (learnset[move].some(learned => learned.startsWith('1'))) { pool.push(move); } } } team.push({ name: species.baseSpecies, species: species.name, moves: this.multipleSamplesNoReplace(pool, 4), gender: false, ability: 'No Ability', evs: evs, ivs: ivs, item: '', level, happiness: 0, shiny: false, nature: 'Serious', }); } return team; } // Random team generation for Gen 1 Random Battles. randomTeam() { this.enforceNoDirectCustomBanlistChanges(); // Get what we need ready. const seed = this.prng.seed; const ruleTable = this.dex.formats.getRuleTable(this.format); const pokemon: RandomTeamsTypes.RandomSet[] = []; // For Monotype const isMonotype = !!this.forceMonotype || ruleTable.has('sametypeclause'); const typePool = this.dex.types.names(); const type = this.forceMonotype || this.sample(typePool); /** Pokémon that are not wholly incompatible with the team, but still pretty bad */ const rejectedButNotInvalidPool: string[] = []; const handicapMons = ['magikarp', 'weedle', 'kakuna', 'caterpie', 'metapod']; const nuTiers = ['UU', 'UUBL', 'NFE', 'LC', 'NU']; const uuTiers = ['NFE', 'UU', 'UUBL', 'NU']; // Now let's store what we are getting. const typeCount: {[k: string]: number} = {}; const weaknessCount: {[k: string]: number} = {Electric: 0, Psychic: 0, Water: 0, Ice: 0, Ground: 0}; let uberCount = 0; let nuCount = 0; let hasShitmon = false; const pokemonPool = this.getPokemonPool(type, pokemon, isMonotype); while (pokemonPool.length && pokemon.length < this.maxTeamSize) { const species = this.dex.species.get(this.sampleNoReplace(pokemonPool)); if (!species.exists || !species.randomBattleMoves) continue; // Only one Ditto is allowed per battle in Generation 1, // as it can cause an endless battle if two Dittos are forced // to face each other. if (species.id === 'ditto' && this.battleHasDitto) continue; // Really bad Pokémon shouldn't be leads. if (pokemon.length === 0 && handicapMons.includes(species.id)) continue; // Bias the tiers so you get less shitmons and only one of the two Ubers. // If you have a shitmon, don't get another if (handicapMons.includes(species.id) && hasShitmon) { rejectedButNotInvalidPool.push(species.id); continue; } // Dynamically scale limits for different team sizes. The default and minimum value is 1. const limitFactor = Math.round(this.maxTeamSize / 6) || 1; const tier = species.tier; switch (tier) { case 'LC': case 'NFE': // Don't add pre-evo mon if already 4 or more non-OUs, or if already 3 or more non-OUs with one being a shitmon // Regardless, pre-evo mons are slightly less common. if (nuCount >= 4 * limitFactor || (hasShitmon && nuCount >= 4 * limitFactor - 1) || this.randomChance(1, 3)) continue; break; case 'Uber': // If you have one of the worst mons we allow luck to give you all Ubers. if (uberCount >= 1 * limitFactor && !hasShitmon) continue; break; default: // OUs are fine. Otherwise 50% chance to skip mon if already 4 or more non-OUs. if (uuTiers.includes(tier) && pokemonPool.length > 1 && (nuCount >= 4 * limitFactor && this.randomChance(1, 2))) { continue; } } let skip = false; if (!isMonotype && !this.forceMonotype) { // Limit 2 of any type as well. Diversity and minor weakness count. // The second of a same type has halved chance of being added. for (const typeName of species.types) { if (typeCount[typeName] >= 2 * limitFactor || (typeCount[typeName] >= 1 * limitFactor && this.randomChance(1, 2) && pokemonPool.length > 1)) { skip = true; break; } } if (skip) { rejectedButNotInvalidPool.push(species.id); continue; } } // We need a weakness count of spammable attacks to avoid being swept by those. // Spammable attacks are: Thunderbolt, Psychic, Surf, Blizzard, Earthquake. const pokemonWeaknesses = []; for (const typeName in weaknessCount) { const increaseCount = this.dex.getImmunity(typeName, species) && this.dex.getEffectiveness(typeName, species) > 0; if (!increaseCount) continue; if (weaknessCount[typeName] >= 2 * limitFactor) { skip = true; break; } pokemonWeaknesses.push(typeName); } if (skip) { rejectedButNotInvalidPool.push(species.id); continue; } // The set passes the limitations. pokemon.push(this.randomSet(species)); // Now let's increase the counters. // Type counter. for (const typeName of species.types) { if (typeCount[typeName]) { typeCount[typeName]++; } else { typeCount[typeName] = 1; } } // Weakness counter. for (const weakness of pokemonWeaknesses) { weaknessCount[weakness]++; } // Increment tier bias counters. if (tier === 'Uber') { uberCount++; } else if (nuTiers.includes(tier)) { nuCount++; } // Is it Magikarp or one of the useless bugs? if (handicapMons.includes(species.id)) hasShitmon = true; // Ditto check if (species.id === 'ditto') this.battleHasDitto = true; } // if we don't have enough Pokémon, go back to rejects, which are already known to not be invalid. while (pokemon.length < this.maxTeamSize && rejectedButNotInvalidPool.length) { const species = this.sampleNoReplace(rejectedButNotInvalidPool); pokemon.push(this.randomSet(species)); } if (pokemon.length < this.maxTeamSize && pokemon.length < 12 && !isMonotype) { throw new Error(`Could not build a random team for ${this.format} (seed=${seed})`); } return pokemon; } shouldCullMove(move: Move, types: Set, moves: Set, counter: MoveCounter): {cull: boolean} { switch (move.id) { // bit redundant to have both, but neither particularly better than the other case 'hydropump': return {cull: moves.has('surf')}; case 'surf': return {cull: moves.has('hydropump')}; // other redundancies that aren't handled within the movesets themselves case 'selfdestruct': return {cull: moves.has('rest')}; case 'rest': return {cull: moves.has('selfdestruct')}; case 'sharpen': case 'swordsdance': return {cull: counter.get('Special') > counter.get('Physical') || !counter.get('Physical') || moves.has('growth')}; case 'growth': return {cull: counter.get('Special') < counter.get('Physical') || !counter.get('Special') || moves.has('swordsdance')}; case 'poisonpowder': case 'stunspore': case 'sleeppowder': case 'toxic': return {cull: counter.get('Status') > 1}; } return {cull: false}; } /** * Random set generation for Gen 1 Random Battles. */ randomSet(species: string | Species): RandomTeamsTypes.RandomSet { species = this.dex.species.get(species); if (!species.exists) species = this.dex.species.get('pikachu'); // Because Gen 1. const movePool = species.randomBattleMoves ? species.randomBattleMoves.slice() : []; const moves = new Set(); const types = new Set(species.types); const counter = new MoveCounter(); // Moves that boost Attack: const PhysicalSetup = ['swordsdance', 'sharpen']; // Moves which boost Special Attack: const SpecialSetup = ['amnesia', 'growth']; // Either add all moves or add none if (species.comboMoves && species.comboMoves.length <= this.maxMoveCount && this.randomChance(1, 2)) { for (const m of species.comboMoves) moves.add(m); } // Add one of the semi-mandatory moves // Often, these are used so that the Pokemon only gets one of the less useful moves if (moves.size < this.maxMoveCount && species.exclusiveMoves) { moves.add(this.sample(species.exclusiveMoves)); } // Add the mandatory move. SD Mew and Amnesia Snorlax are exceptions. if (moves.size < this.maxMoveCount && species.essentialMove) { moves.add(species.essentialMove); } while (moves.size < this.maxMoveCount && movePool.length) { // Choose next 4 moves from learnset/viable moves and add them to moves list: while (moves.size < this.maxMoveCount && movePool.length) { const moveid = this.sampleNoReplace(movePool); moves.add(moveid); } // Only do move choosing if we have backup moves in the pool... if (movePool.length) { for (const setMoveid of moves) { const move = this.dex.moves.get(setMoveid); const moveid = move.id; if (!move.damage && !move.damageCallback) counter.add(move.category); if (PhysicalSetup.includes(moveid)) counter.add('physicalsetup'); if (SpecialSetup.includes(moveid)) counter.add('specialsetup'); } for (const moveid of moves) { if (moveid === species.essentialMove) continue; const move = this.dex.moves.get(moveid); if ( (!species.essentialMove || moveid !== species.essentialMove) && this.shouldCullMove(move, types, moves, counter).cull ) { moves.delete(moveid); break; } counter.add(move.category); } } // End of the check for more than 4 moves on moveset. } const levelScale: {[k: string]: number} = { LC: 88, NFE: 80, PU: 77, NU: 77, NUBL: 76, UU: 74, '(OU)': 71, OU: 68, Uber: 65, }; const customScale: {[k: string]: number} = { Mewtwo: 62, Caterpie: 100, Metapod: 100, Weedle: 100, Kakuna: 100, Magikarp: 100, Ditto: 88, }; const level = this.adjustLevel || customScale[species.name] || levelScale[species.tier] || 80; return { name: species.name, species: species.name, moves: Array.from(moves), ability: 'No Ability', evs: {hp: 255, atk: 255, def: 255, spa: 255, spd: 255, spe: 255}, ivs: {hp: 30, atk: 30, def: 30, spa: 30, spd: 30, spe: 30}, item: '', level, shiny: false, gender: false, }; } randomHCTeam(): PokemonSet[] { this.enforceNoDirectCustomBanlistChanges(); const team = []; const movePool = [...this.dex.moves.all()]; const typesPool = ['Bird', ...this.dex.types.names()]; const randomN = this.randomNPokemon(this.maxTeamSize); const hackmonsCup: {[k: string]: HackmonsCupEntry} = {}; for (const forme of randomN) { // Choose forme const species = this.dex.species.get(forme); if (!hackmonsCup[species.id]) { hackmonsCup[species.id] = { types: [this.sample(typesPool), this.sample(typesPool)], baseStats: { hp: Utils.clampIntRange(this.random(256), 1), atk: Utils.clampIntRange(this.random(256), 1), def: Utils.clampIntRange(this.random(256), 1), spa: Utils.clampIntRange(this.random(256), 1), spd: 0, spe: Utils.clampIntRange(this.random(256), 1), }, }; if (this.forceMonotype && !hackmonsCup[species.id].types.includes(this.forceMonotype)) { hackmonsCup[species.id].types[1] = this.forceMonotype; } hackmonsCup[species.id].baseStats.spd = hackmonsCup[species.id].baseStats.spa; } if (hackmonsCup[species.id].types[0] === hackmonsCup[species.id].types[1]) { hackmonsCup[species.id].types.splice(1, 1); } // Random unique moves const moves = []; do { const move = this.sampleNoReplace(movePool); if (move.gen <= this.gen && !move.isNonstandard && !move.name.startsWith('Hidden Power ')) { moves.push(move.id); } } while (moves.length < this.maxMoveCount); // Random EVs const evs = { hp: this.random(256), atk: this.random(256), def: this.random(256), spa: this.random(256), spd: 0, spe: this.random(256), }; evs['spd'] = evs['spa']; // Random DVs const ivs: StatsTable = { hp: 0, atk: this.random(16), def: this.random(16), spa: this.random(16), spd: 0, spe: this.random(16), }; ivs["hp"] = (ivs["atk"] % 2) * 16 + (ivs["def"] % 2) * 8 + (ivs["spe"] % 2) * 4 + (ivs["spa"] % 2) * 2; for (const iv in ivs) { if (iv === 'hp' || iv === 'spd') continue; ivs[iv as keyof StatsTable] *= 2; } ivs['spd'] = ivs['spa']; // Level balance const mbstmin = 425; const baseStats = hackmonsCup[species.id].baseStats; const calcStat = (statName: StatID, lvl?: number) => { if (lvl) { return Math.floor(Math.floor(2 * baseStats[statName] + ivs[statName] + Math.floor(evs[statName] / 4)) * lvl / 100 + 5); } return Math.floor(2 * baseStats[statName] + ivs[statName] + Math.floor(evs[statName] / 4)) + 5; }; let mbst = 0; for (const statName of Object.keys(baseStats)) { mbst += calcStat(statName as StatID); if (statName === 'hp') mbst += 5; } let level; if (this.adjustLevel) { level = this.adjustLevel; } else { level = Math.floor(100 * mbstmin / mbst); while (level < 100) { for (const statName of Object.keys(baseStats)) { mbst += calcStat(statName as StatID, level); if (statName === 'hp') mbst += 5; } if (mbst >= mbstmin) break; level++; } if (level > 100) level = 100; } team.push({ name: species.baseSpecies, species: species.name, gender: species.gender, item: '', ability: 'No Ability', moves, evs, ivs, nature: '', level, shiny: false, // Hacky but the only way to communicate stats/level generation properly hc: hackmonsCup[species.id], }); } return team; } } export default RandomGen1Teams;