From ddb6010bb92963fb50771aaf5a052fff29a82135 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Wed, 9 Jun 2021 22:01:31 -0700 Subject: [PATCH] Support importing teams We now have a `Teams.import` function. This supports importing teams in any format, allowing it to be the backbone of a new series of commandline functions, which support teams in any format. --- COMMANDLINE.md | 84 ++++++++++++++++++++++-- pokemon-showdown | 51 ++++++++++---- sim/TEAMS.md | 6 +- sim/dex-data.ts | 17 +++++ sim/teams.ts | 168 ++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 308 insertions(+), 18 deletions(-) diff --git a/COMMANDLINE.md b/COMMANDLINE.md index 909623e201..e78fc3bef5 100644 --- a/COMMANDLINE.md +++ b/COMMANDLINE.md @@ -40,7 +40,7 @@ Note: Commands that ask for a team want the team in [packed team format][packed- `./pokemon-showdown validate-team [FORMAT-ID]` -: Reads a team from stdin, and validates it +: Reads a team in any format from stdin, and validates it : - If valid: exits with code 0 : - If invalid: writes errors to stderr, exits with code 1 @@ -51,14 +51,90 @@ Note: Commands that ask for a team want the team in [packed team format][packed- : Using Pokémon Showdown as a command-line simulator is documented at: : https://github.com/smogon/pokemon-showdown/blob/master/sim/README.md -`./pokemon-showdown unpack-team` +`./pokemon-showdown json-team` -: Reads a team from stdin, writes the unpacked JSON to stdout +: Reads a team in any format from stdin, writes the unpacked JSON to stdout `./pokemon-showdown pack-team` -: Reads a JSON team from stdin, writes the packed team to stdout +: Reads a team in any format from stdin, writes the packed team to stdout + +`./pokemon-showdown export-team` + +: Reads a team in any format from stdin, writes the exported (human-readable) team to stdout `./pokemon-showdown help` : Displays this reference + + +Piping +------ + +These commands are very unixy (using stdin and stdout), so you can of course pipe them together: + +``` +$ ./pokemon-showdown generate-team gen8randombattle | ./pokemon-showdown export-team +Kartana @ Choice Band +Ability: Beast Boost +Level: 74 +EVs: 85 HP / 85 Atk / 85 Def / 85 SpA / 85 SpD / 85 Spe +- Smart Strike +- Sacred Sword +- Knock Off +- Leaf Blade + +Rotom (Rotom-Heat) @ Heavy-Duty Boots +Ability: Levitate +Level: 82 +EVs: 85 HP / 85 Def / 85 SpA / 85 SpD / 85 Spe +IVs: 0 Atk +- Defog +- Will-O-Wisp +- Thunderbolt +- Overheat + +Kingler @ Life Orb +Ability: Sheer Force +Level: 84 +EVs: 85 HP / 85 Atk / 85 Def / 85 SpA / 85 SpD / 85 Spe +- Liquidation +- X-Scissor +- Superpower +- Rock Slide + +Abomasnow @ Light Clay +Ability: Snow Warning +Level: 82 +EVs: 85 HP / 85 Atk / 85 Def / 85 SpA / 85 SpD / 85 Spe +- Ice Shard +- Aurora Veil +- Earthquake +- Blizzard + +Goodra @ Assault Vest +Ability: Sap Sipper +Level: 82 +EVs: 85 HP / 85 Atk / 85 Def / 85 SpA / 85 SpD / 85 Spe +- Earthquake +- Power Whip +- Draco Meteor +- Fire Blast + +Raikou @ Choice Specs +Ability: Pressure +Level: 80 +EVs: 85 HP / 85 Def / 85 SpA / 85 SpD / 85 Spe +IVs: 0 Atk +- Scald +- Aura Sphere +- Thunderbolt +- Volt Switch +``` + +``` +$ ./pokemon-showdown generate-team gen8randombattle | ./pokemon-showdown validate-team gen8ou +Your set for Coalossal is flagged as Gigantamax, but Gigantamaxing is disallowed +(If this was a mistake, disable Gigantamaxing on the set.) +Octillery's ability Moody is banned. +``` diff --git a/pokemon-showdown b/pokemon-showdown index 7c0f6b82c1..f26c0f1e4c 100755 --- a/pokemon-showdown +++ b/pokemon-showdown @@ -29,6 +29,13 @@ try { build(); } +function readTeam(stream) { + return stream.readLine().then(line => { + if (line.startsWith('[') || line.includes('|')) return line; + return stream.readAll().then(all => (line + '\n' + all)); + }); +} + if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { // Start the server. // @@ -68,16 +75,17 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { console.log(' Simulates a battle, taking input to stdin and writing output to stdout'); console.log(' Protocol is documented in ./.sim-dist/README.md'); console.log(''); - console.log('pokemon-showdown unpack-team'); + console.log('pokemon-showdown json-team'); console.log(''); - console.log(' Reads a team from stdin, writes the unpacked JSON to stdout'); + console.log(' Reads a team in any format from stdin, writes the unpacked JSON to stdout'); console.log(''); console.log('pokemon-showdown pack-team'); console.log(''); - console.log(' Reads a JSON team from stdin, writes the packed team to stdout'); - console.log(' NOTE for all team-processing functions: We can only handle JSON teams'); - console.log(' and packed teams; the PS server is incapable of processing exported'); - console.log(' teams.'); + console.log(' Reads a team in any format from stdin, writes the packed team to stdout'); + console.log(''); + console.log('pokemon-showdown export-team'); + console.log(''); + console.log(' Reads a team in any format from stdin, writes the exported (human-readable) team to stdout'); console.log(''); console.log('pokemon-showdown help'); console.log(''); @@ -108,9 +116,9 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { var Streams = require('./.lib-dist/streams'); var stdin = Streams.stdin(); - stdin.readLine().then(function (textTeam) { + readTeam(stdin).then(function (textTeam) { try { - var team = Teams.unpack(textTeam); + var team = Teams.import(textTeam); var result = validator.validateTeam(team); if (result) { console.error(result.join('\n')); @@ -175,14 +183,15 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { } break; case 'unpack-team': + case 'json-team': { var Teams = require('./.sim-dist/teams').Teams; var Streams = require('./.lib-dist/streams'); var stdin = Streams.stdin(); - stdin.readLine().then(function (packedTeam) { + readTeam(stdin).then(function (team) { try { - var unpackedTeam = Teams.unpack(packedTeam); + var unpackedTeam = Teams.unpack(Teams.import(team)); console.log(JSON.stringify(unpackedTeam)); process.exit(0); } catch (e) { @@ -198,9 +207,9 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { var Streams = require('./.lib-dist/streams'); var stdin = Streams.stdin(); - stdin.readLine().then(function (unpackedTeam) { + readTeam(stdin).then(function (team) { try { - var packedTeam = Teams.pack(JSON.parse(unpackedTeam)); + var packedTeam = Teams.pack(Teams.import(team)); console.log(packedTeam); process.exit(0); } catch (e) { @@ -210,6 +219,24 @@ if (!process.argv[2] || /^[0-9]+$/.test(process.argv[2])) { }); } break; + case 'export-team': + { + var Teams = require('./.sim-dist/teams').Teams; + var Streams = require('./.lib-dist/streams'); + var stdin = Streams.stdin(); + + readTeam(stdin).then(function (team) { + try { + var exportedTeam = Teams.export(Teams.import(team)); + console.log(exportedTeam); + process.exit(0); + } catch (e) { + console.error(e); + process.exit(1); + } + }); + } + break; default: console.error('Unrecognized command: ' + process.argv[2]); console.error('Use `pokemon-showdown help` for help'); diff --git a/sim/TEAMS.md b/sim/TEAMS.md index 7c2fc9840a..4965f332a2 100644 --- a/sim/TEAMS.md +++ b/sim/TEAMS.md @@ -173,6 +173,10 @@ API: - Converts a JSON team to a packed team +`Teams.import(exportedTeam: string): PokemonSet[]` + +- Converts a team in any string format (JSON, exported, or packed) to a JSON team + `Teams.export(team: PokemonSet[]): string` - Converts a JSON team to an export team @@ -181,7 +185,7 @@ API: - Converts a JSON set to export format -(Import is not available in this version; we'll add it to a future version.) +To convert from export to packed (or vice versa), just round-trip through PokemonSet: `Teams.export(Teams.unpack(packedTeam))` will produce an exported team. Example use: diff --git a/sim/dex-data.ts b/sim/dex-data.ts index 1e71d2c2e1..fa0912fd53 100644 --- a/sim/dex-data.ts +++ b/sim/dex-data.ts @@ -307,6 +307,16 @@ export class DexTypes { } const idsCache: readonly StatID[] = ['hp', 'atk', 'def', 'spa', 'spd', 'spe']; +const reverseCache: {readonly [k: string]: StatID} = { + __proto: null as any, + "hitpoints": 'hp', + "attack": 'atk', + "defense": 'def', + "specialattack": 'spa', "spatk": 'spa', "spattack": 'spa', "specialatk": 'spa', + "special": 'spa', "spc": 'spa', + "specialdefense": 'spd', "spdef": 'spd', "spdefense": 'spd', "specialdef": 'spd', + "speed": 'spe', +}; export class DexStats { readonly shortNames: {readonly [k in StatID]: string}; readonly mediumNames: {readonly [k in StatID]: string}; @@ -334,6 +344,13 @@ export class DexStats { } as any; } } + getID(name: string) { + if (name === 'Spd') return 'spe' as StatID; + const id = toID(name); + if (reverseCache[id]) return reverseCache[id]; + if (idsCache.includes(id as StatID)) return id as StatID; + return null; + } ids(): typeof idsCache { return idsCache; } diff --git a/sim/teams.ts b/sim/teams.ts index 2e40f60bfa..e69a89f6c1 100644 --- a/sim/teams.ts +++ b/sim/teams.ts @@ -7,7 +7,7 @@ * @license MIT */ -import {Dex} from './dex'; +import {Dex, toID} from './dex'; import type {PRNG, PRNGSeed} from './prng'; export interface PokemonSet { @@ -421,6 +421,172 @@ export const Teams = new class Teams { return out; } + parseExportedTeamLine(line: string, isFirstLine: boolean, set: PokemonSet) { + if (isFirstLine) { + let item; + [line, item] = line.split(' @ '); + if (item) { + set.item = item; + if (toID(set.item) === 'noitem') set.item = ''; + } + if (line.endsWith(' (M)')) { + set.gender = 'M'; + line = line.slice(0, -4); + } + if (line.endsWith(' (F)')) { + set.gender = 'F'; + line = line.slice(0, -4); + } + if (line.endsWith(')') && line.includes('(')) { + const [name, species] = line.slice(0, -1).split('('); + set.species = Dex.species.get(species).name; + set.name = name.trim(); + } else { + set.species = Dex.species.get(line).name; + set.name = ''; + } + } else if (line.startsWith('Trait: ')) { + line = line.slice(7); + set.ability = line; + } else if (line.startsWith('Ability: ')) { + line = line.slice(9); + set.ability = line; + } else if (line === 'Shiny: Yes') { + set.shiny = true; + } else if (line.startsWith('Level: ')) { + line = line.slice(7); + set.level = +line; + } else if (line.startsWith('Happiness: ')) { + line = line.slice(11); + set.happiness = +line; + } else if (line.startsWith('Pokeball: ')) { + line = line.slice(10); + set.pokeball = line; + } else if (line.startsWith('Hidden Power: ')) { + line = line.slice(14); + set.hpType = line; + } else if (line === 'Gigantamax: Yes') { + set.gigantamax = true; + } else if (line.startsWith('EVs: ')) { + line = line.slice(5); + const evLines = line.split('/'); + set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; + for (const evLine of evLines) { + const [statValue, statName] = evLine.trim().split(' '); + const statid = Dex.stats.getID(statName); + if (!statid) continue; + const value = parseInt(statValue); + set.evs[statid] = value; + } + } else if (line.startsWith('IVs: ')) { + line = line.slice(5); + const ivLines = line.split('/'); + set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; + for (const ivLine of ivLines) { + const [statValue, statName] = ivLine.trim().split(' '); + const statid = Dex.stats.getID(statName); + if (!statid) continue; + let value = parseInt(statValue); + if (isNaN(value)) value = 31; + set.ivs[statid] = value; + } + } else if (/^[A-Za-z]+ (N|n)ature/.test(line)) { + let natureIndex = line.indexOf(' Nature'); + if (natureIndex === -1) natureIndex = line.indexOf(' nature'); + if (natureIndex === -1) return; + line = line.substr(0, natureIndex); + if (line !== 'undefined') set.nature = line; + } else if (line.startsWith('-') || line.startsWith('~')) { + line = line.slice(line.charAt(1) === ' ' ? 2 : 1); + if (line.startsWith('Hidden Power [')) { + const hpType = line.slice(14, -1); + line = 'Hidden Power ' + hpType; + if (!set.ivs && Dex.types.isName(hpType)) { + set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; + const hpIVs = Dex.types.get(hpType).HPivs || {}; + for (const statid in hpIVs) { + set.ivs[statid as StatID] = hpIVs[statid as StatID]!; + } + } + } + if (line === 'Frustration' && set.happiness === undefined) { + set.happiness = 0; + } + set.moves.push(line); + } + } + /** Accepts a team in any format (JSON, packed, or exported) */ + import(buffer: string): PokemonSet[] | null { + if (buffer.startsWith('[')) { + try { + const team = JSON.parse(buffer); + if (!Array.isArray(team)) throw new Error(`Team should be an Array but isn't`); + for (const set of team) { + set.name = Dex.getName(set.name); + set.species = Dex.getName(set.species); + set.item = Dex.getName(set.item); + set.ability = Dex.getName(set.ability); + set.gender = Dex.getName(set.gender); + set.nature = Dex.getName(set.nature); + const evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; + if (set.evs) { + for (const statid in evs) { + if (typeof set.evs[statid] === 'number') evs[statid as StatID] = set.evs[statid]; + } + } + set.evs = evs; + const ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; + if (set.ivs) { + for (const statid in ivs) { + if (typeof set.ivs[statid] === 'number') ivs[statid as StatID] = set.ivs[statid]; + } + } + set.ivs = ivs; + if (!Array.isArray(set.moves)) { + set.moves = []; + } else { + set.moves = set.moves.map(Dex.getName); + } + } + return team; + } catch (e) {} + } + + const lines = buffer.split("\n"); + + const sets: PokemonSet[] = []; + let curSet: PokemonSet | null = null; + + while (lines.length && !lines[0]) lines.shift(); + while (lines.length && !lines[lines.length - 1]) lines.pop(); + + if (lines.length === 1 && lines[0].includes('|')) { + return this.unpack(lines[0]); + } + for (let line of lines) { + line = line.trim(); + if (line === '' || line === '---') { + curSet = null; + } else if (line.startsWith('===')) { + // team backup format; ignore + } else if (!curSet) { + curSet = { + name: '', species: '', item: '', ability: '', gender: '', + nature: '', + evs: {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}, + ivs: {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}, + level: 100, + moves: [], + }; + sets.push(curSet); + this.parseExportedTeamLine(line, true, curSet); + } else { + this.parseExportedTeamLine(line, false, curSet); + } + } + return sets; + } + getGenerator(format: Format | string, seed: PRNG | PRNGSeed | null = null) { const TeamGenerator = require(Dex.forFormat(format).dataDir + '/random-teams').default; return new TeamGenerator(format, seed);