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.
This commit is contained in:
Guangcong Luo 2021-06-09 22:01:31 -07:00
parent 5d3b758564
commit ddb6010bb9
5 changed files with 308 additions and 18 deletions

View File

@ -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.
```

View File

@ -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');

View File

@ -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:

View File

@ -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;
}

View File

@ -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);