300 lines
7.3 KiB
JavaScript
300 lines
7.3 KiB
JavaScript
|
import calc, {
|
||
|
calculate,
|
||
|
Generations,
|
||
|
Field,
|
||
|
Move,
|
||
|
Pokemon,
|
||
|
} from "@smogon/calc";
|
||
|
import assert from "assert";
|
||
|
import chev, { Lexer } from "chevrotain";
|
||
|
|
||
|
/**
|
||
|
* Creates a lexer.
|
||
|
*
|
||
|
* @returns {Lexer}
|
||
|
*/
|
||
|
function buildLexer() {
|
||
|
const Boost = chev.createToken({ name: "Boost", pattern: /[+-]\d+/ });
|
||
|
const EV = chev.createToken({ name: "EV", pattern: /\d+[+-]?/ });
|
||
|
const Stat = chev.createToken({
|
||
|
name: "Stat",
|
||
|
pattern: /HP|Atk|Def|SpA|SpD|Spe/,
|
||
|
});
|
||
|
|
||
|
const Item = chev.createToken({
|
||
|
name: "Item",
|
||
|
pattern: new RegExp(calc.ITEMS.flatMap((gen) => gen).join("|")),
|
||
|
});
|
||
|
const Ability = chev.createToken({
|
||
|
name: "Ability",
|
||
|
pattern: new RegExp(calc.ABILITIES.flatMap((gen) => gen).join("|")),
|
||
|
});
|
||
|
|
||
|
const Pokemon = chev.createToken({
|
||
|
name: "Pokemon",
|
||
|
pattern: new RegExp(
|
||
|
calc.SPECIES.flatMap((gen) => Object.keys(gen)).join("|")
|
||
|
),
|
||
|
});
|
||
|
const Move = chev.createToken({
|
||
|
name: "Move",
|
||
|
pattern: new RegExp(
|
||
|
calc.MOVES.flatMap((gen) => Object.keys(gen)).join("|")
|
||
|
),
|
||
|
});
|
||
|
|
||
|
const whitespace = chev.createToken({
|
||
|
name: "Whitespace",
|
||
|
pattern: /\s+/,
|
||
|
group: Lexer.SKIPPED,
|
||
|
});
|
||
|
const div = chev.createToken({
|
||
|
name: "div",
|
||
|
pattern: "/",
|
||
|
group: Lexer.SKIPPED,
|
||
|
});
|
||
|
const vs = chev.createToken({
|
||
|
name: "vs",
|
||
|
pattern: "vs.",
|
||
|
});
|
||
|
|
||
|
const terrainEnter = chev.createToken({
|
||
|
name: "terrainEnter",
|
||
|
pattern: "in",
|
||
|
push_mode: "terrain_mode",
|
||
|
});
|
||
|
const Weather = chev.createToken({
|
||
|
name: "Weather",
|
||
|
pattern: /Sun|Rain|Sand|Snow/,
|
||
|
});
|
||
|
const Terrain = chev.createToken({
|
||
|
name: "Terrain",
|
||
|
pattern: /(Electric|Grassy|Misty|Psychic) Terrain/,
|
||
|
});
|
||
|
|
||
|
const screenEnter = chev.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 {chev.IToken} token token
|
||
|
* @return {string} matched token
|
||
|
*/
|
||
|
function unwrapToken(type, token) {
|
||
|
assert(
|
||
|
token.tokenType.name == type,
|
||
|
"expected token %s, got %s",
|
||
|
type,
|
||
|
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 {calc.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 {calc.State.Pokemon} */
|
||
|
var attackerOpts = {};
|
||
|
/** @type {calc.State.Pokemon} */
|
||
|
var defenderOpts = {};
|
||
|
|
||
|
/** @type {string} */
|
||
|
var move;
|
||
|
/** @type {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("Impossibel state: bare Stat");
|
||
|
case "Item":
|
||
|
opts().item = unwrapToken("Item", 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const gen = Generations.get(8);
|
||
|
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 };
|