import { calculate, toID, Field, Generations, Move, Pokemon, Result, } from "@ajhyndman/smogon-calc"; import assert from "assert"; import { createToken, Lexer } from "chevrotain"; const gen = Generations.get(9); /** * Creates a lexer. * * @returns {Lexer} */ function buildLexer() { const Boost = createToken({ name: "Boost", pattern: /[+-]\d+/ }); const EV = createToken({ name: "EV", pattern: /\d+[+-]?/ }); const Stat = createToken({ name: "Stat", pattern: /HP|Atk|Def|SpA|SpD|Spe/, }); const Item = createToken({ name: "Item", pattern: new RegExp([...gen.items].map((i) => i.name).join("|")), }); const Ability = createToken({ name: "Ability", pattern: new RegExp([...gen.abilities].map((a) => a.name).join("|")), }); const Pokemon = createToken({ name: "Pokemon", pattern: new RegExp([...gen.species].map((s) => s.name).join("|")), }); const Move = createToken({ name: "Move", pattern: new RegExp([...gen.moves].map((m) => m.name).join("|")), }); const whitespace = createToken({ name: "Whitespace", pattern: /\s+/, group: Lexer.SKIPPED, }); const div = createToken({ name: "div", pattern: "/", group: Lexer.SKIPPED, }); const vs = createToken({ name: "vs", pattern: /vs\.?/, }); const terrainEnter = createToken({ name: "terrainEnter", pattern: "in", push_mode: "terrain_mode", }); const Weather = createToken({ name: "Weather", pattern: /Sun|Rain|Sand|Snow/, }); const Terrain = createToken({ name: "Terrain", pattern: /(Electric|Grassy|Misty|Psychic) Terrain/, }); const screenEnter = createToken({ name: "screenEnter", pattern: "through", push_mode: "screen_mode", }); return new Lexer({ modes: { default_mode: [ whitespace, div, vs, Boost, EV, Stat, Item, Ability, Pokemon, Move, terrainEnter, screenEnter, ], terrain_mode: [whitespace, screenEnter, Weather, Terrain], screen_mode: [whitespace, Move], }, defaultMode: "default_mode", }); } /** * @generator * @template T item type * @param {T[]} arr * @yields {T} item */ function* iterate(arr) { for (const a of arr) { yield a; } } /** * @param {string} type token type * @param {import("chevrotain").IToken} token token * @return {string} matched token */ function unwrapToken(type, token) { assert( token.tokenType.name == type, `expected token ${type}, got ${token.tokenType.name}`, ); return token.image; } const POS_NATURES = { atk: "Adamant", def: "Bold", spa: "Modest", spd: "Calm", spe: "Jolly", }; const NEG_NATURES = { atk: "Modest", def: "Lonely", spa: "Adamant", spd: "Rash", spe: "Brave", }; /** * Parses a Smogon calculator output: * * -2 8 SpA Choice Specs Torkoal vs. * 252 HP / 4+ SpD Assault Vest Abomasnow * in Sun * through Light Screen * * @param {string} line textual line * @return {Result} calculation result */ function parseAndCalculate(line) { const lexer = buildLexer(); const result = lexer.tokenize(line); if (result.errors && result.errors.length > 0) { console.error("Unparsed tokens: %o", result.errors); } /** @type {string} */ var attacker; /** @type {string} */ var defender; /** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */ var attackerOpts = {}; /** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */ var defenderOpts = {}; /** @type {string} */ var move; /** @type {import("@ajhyndman/smogon-calc").State.Field} */ var field = {}; // Tokenising state. var isAttacker = true; var it = iterate(result.tokens); const opts = () => (isAttacker ? attackerOpts : defenderOpts); while (true) { let item = it.next().value; if (!item) break; switch (item.tokenType.name) { case "Boost": { let boost = unwrapToken("Boost", item); let ev = unwrapToken("EV", it.next().value); let stat = unwrapToken("Stat", it.next().value).toLowerCase(); opts().boosts = { [stat]: parseInt(boost), ...opts().boosts }; opts().evs = { [stat]: parseInt(ev), ...opts().evs }; if (ev.endsWith("+")) { opts().nature = POS_NATURES[stat]; } else if (ev.endsWith("-")) { opts().nature = NEG_NATURES[stat]; } } break; case "EV": { let ev = unwrapToken("EV", item); let stat = unwrapToken("Stat", it.next().value).toLowerCase(); opts().evs = { [stat]: parseInt(ev), ...opts().evs }; if (ev.endsWith("+")) { opts().nature = POS_NATURES[stat]; } else if (ev.endsWith("-")) { opts().nature = NEG_NATURES[stat]; } } break; case "Stat": throw Error("Impossible state: bare Stat"); case "Item": opts().item = unwrapToken("Item", item); break; case "Ability": opts().ability = unwrapToken("Ability", item); break; case "Pokemon": { let name = unwrapToken("Pokemon", item); if (isAttacker) attacker = name; else defender = name; } break; case "Move": move = unwrapToken("Move", item); break; case "terrainEnter": { let next = it.next().value; switch (next.tokenType.name) { case "Weather": field.weather = next.image; break; case "Terrain": field.terrain = next.image; break; default: throw Error( "Unhandled terrain %s: %s", next.tokenType.name, next.image ); } } break; case "screenEnter": { let screen = unwrapToken("Move", it.next().value); field.defenderSide = field.defenderSide || {}; field.defenderSide.isLightScreen = screen === "Light Screen"; field.defenderSide.isReflect = screen === "Reflect"; field.defenderSide.isAuroraVeil = screen === "Aurora Veil"; field.defenderSide.is; } break; case "vs": isAttacker = false; break; default: console.error("unmatched token type: %s", item.tokenType.name); break; } } // Pre-checking before the calculator throws unreadable errors. if (!gen.species.get(toID(attacker))) throw Error(`No species named ${attacker}`); if (!gen.species.get(toID(defender))) throw Error(`No species named ${defender}`); if (!gen.moves.get(toID(move))) throw Error(`No move named ${move}`); return calculate( gen, new Pokemon(gen, attacker, attackerOpts), new Pokemon(gen, defender, defenderOpts), new Move(gen, move), new Field(field) ); } function test() { const text = "-2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen"; const res = parseAndCalculate(text); assert(res.attacker.boosts.spa === -2, "should have -2 SpA"); assert(res.attacker.evs.spa === 8, "should have 8 SpA EVs"); assert(res.attacker.item === "Choice Specs", "should have Choice Specs"); assert(res.attacker.name === "Torkoal", "should be Torkoal"); assert(res.move.name === "Overheat", "should be Overheat"); assert(res.defender.evs.hp === 252, "should have 252 HP EVs"); assert(res.defender.evs.spd === 4, "should have 4 SpD EVs"); assert(res.field.weather === "Sun", "should be in sun"); assert( res.desc().replace(/:.*/, "") === text, "non-damage text should be equivalent to input" ); } export { parseAndCalculate, test };