Compare commits

...

5 Commits

Author SHA1 Message Date
ea2285ed1d
update help 2023-04-27 12:16:09 +10:00
6c7d61f916
fix support for mega/formes 2023-04-27 12:11:39 +10:00
2acbf9945c
support tera 2023-04-27 12:11:36 +10:00
6aa1cac328
disallow "in through <screen>" 2023-04-27 11:58:02 +10:00
0b163e8ffd
escape regex tokens 2023-04-27 11:57:44 +10:00
2 changed files with 61 additions and 9 deletions

3
bot.py
View File

@ -93,7 +93,8 @@ class BotClient(discord.Client):
"Run Showdown damage calculations.\n" "Run Showdown damage calculations.\n"
"Example format:\n" "Example format:\n"
"` %calc -2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen`\n" "` %calc -2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen`\n"
"Supported: attacker/defender boosts, EVs, item, species; attacker ability; weather/terrain; screens." "Supported: attacker/defender boosts, EVs, tera, item, ability, species; weather/terrain; screens."
"Not supported (popular): non-default multi hits; hazards."
) )
async def _calculate(self, args: list[str]) -> str: async def _calculate(self, args: list[str]) -> str:

67
calc.js
View File

@ -12,6 +12,14 @@ import { createToken, Lexer } from "chevrotain";
const gen = Generations.get(9); const gen = Generations.get(9);
/**
* @param {string} text
* @return {string}
*/
function escapeRegExp(text) {
return text.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
}
/** /**
* Creates a lexer. * Creates a lexer.
* *
@ -27,20 +35,36 @@ function buildLexer() {
const Item = createToken({ const Item = createToken({
name: "Item", name: "Item",
pattern: new RegExp([...gen.items].map((i) => i.name).join("|")), pattern: new RegExp(
[...gen.items].map((i) => escapeRegExp(i.name)).join("|")
),
}); });
const Ability = createToken({ const Ability = createToken({
name: "Ability", name: "Ability",
pattern: new RegExp([...gen.abilities].map((a) => a.name).join("|")), pattern: new RegExp(
[...gen.abilities].map((a) => escapeRegExp(a.name)).join("|")
),
}); });
const Pokemon = createToken({ const Pokemon = createToken({
name: "Pokemon", name: "Pokemon",
pattern: new RegExp([...gen.species].map((s) => s.name).join("|")), pattern: new RegExp(
[...gen.species]
.flatMap((s) => [
// Important: formes are expanded first because the parser takes the
// first match, and the original species is always a subset of a
// forme.
...(s.otherFormes || []).map(escapeRegExp),
escapeRegExp(s.name),
])
.join("|")
),
}); });
const Move = createToken({ const Move = createToken({
name: "Move", name: "Move",
pattern: new RegExp([...gen.moves].map((m) => m.name).join("|")), pattern: new RegExp(
[...gen.moves].map((m) => escapeRegExp(m.name)).join("|")
),
}); });
const whitespace = createToken({ const whitespace = createToken({
@ -58,6 +82,19 @@ function buildLexer() {
pattern: /vs\.?/, pattern: /vs\.?/,
}); });
const teraEnter = createToken({
name: "teraEnter",
pattern: "Tera",
push_mode: "tera_mode",
});
const Tera = createToken({
name: "Tera",
pattern: new RegExp(
[...gen.types].map((t) => escapeRegExp(t.name)).join("|")
),
pop_mode: true,
});
const terrainEnter = createToken({ const terrainEnter = createToken({
name: "terrainEnter", name: "terrainEnter",
pattern: "in", pattern: "in",
@ -66,10 +103,12 @@ function buildLexer() {
const Weather = createToken({ const Weather = createToken({
name: "Weather", name: "Weather",
pattern: /Sun|Rain|Sand|Snow/, pattern: /Sun|Rain|Sand|Snow/,
pop_mode: true,
}); });
const Terrain = createToken({ const Terrain = createToken({
name: "Terrain", name: "Terrain",
pattern: /(Electric|Grassy|Misty|Psychic) Terrain/, pattern: /(Electric|Grassy|Misty|Psychic) Terrain/,
pop_mode: true,
}); });
const screenEnter = createToken({ const screenEnter = createToken({
@ -91,10 +130,12 @@ function buildLexer() {
Ability, Ability,
Pokemon, Pokemon,
Move, Move,
teraEnter,
terrainEnter, terrainEnter,
screenEnter, screenEnter,
], ],
terrain_mode: [whitespace, screenEnter, Weather, Terrain], tera_mode: [whitespace, Tera],
terrain_mode: [whitespace, Weather, Terrain],
screen_mode: [whitespace, Move], screen_mode: [whitespace, Move],
}, },
defaultMode: "default_mode", defaultMode: "default_mode",
@ -211,8 +252,6 @@ function parseAndCalculate(line) {
} }
} }
break; break;
case "Stat":
throw Error("Impossible state: bare Stat");
case "Item": case "Item":
opts().item = unwrapToken("Item", item); opts().item = unwrapToken("Item", item);
break; break;
@ -229,6 +268,9 @@ function parseAndCalculate(line) {
case "Move": case "Move":
move = unwrapToken("Move", item); move = unwrapToken("Move", item);
break; break;
case "teraEnter":
opts().teraType = unwrapToken("Tera", it.next().value);
break;
case "terrainEnter": case "terrainEnter":
{ {
let next = it.next().value; let next = it.next().value;
@ -284,7 +326,7 @@ function parseAndCalculate(line) {
function test() { function test() {
const text = const text =
"-2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen"; "-2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen";
const res = parseAndCalculate(text); var res = parseAndCalculate(text);
assert(res.attacker.boosts.spa === -2, "should have -2 SpA"); assert(res.attacker.boosts.spa === -2, "should have -2 SpA");
assert(res.attacker.evs.spa === 8, "should have 8 SpA EVs"); assert(res.attacker.evs.spa === 8, "should have 8 SpA EVs");
@ -299,6 +341,15 @@ function test() {
res.desc().replace(/:.*/, "") === text, res.desc().replace(/:.*/, "") === text,
"non-damage text should be equivalent to input" "non-damage text should be equivalent to input"
); );
res = parseAndCalculate("Tera Electric Iron Hands Wild Charge vs Basculin");
assert(res.attacker.teraType === "Electric", "should parse tera type");
res = parseAndCalculate("Gallade-Mega Triple Axel vs Gligar");
assert(res.attacker.name === "Gallade-Mega", "should parse Mega forme");
res = parseAndCalculate("Zoroark-Hisui Night Slash vs Golem");
assert(res.attacker.name === "Zoroark-Hisui", "should parse regional forme");
} }
export { parseAndCalculate, test }; export { parseAndCalculate, test };