Compare commits

..

1 Commits

Author SHA1 Message Date
3a315a0a28 add calculator help 2023-04-27 11:31:39 +10:00
3 changed files with 23 additions and 96 deletions

View File

@ -43,13 +43,3 @@ whatever SQL queries against the data file (default `data.db`) you want.
- calculate gametime based on active turns rather than moves used
- also solves the issue where paralyzed/confused turns are not counted
- use asyncio in bot
- allow for entering unplayed forfeits
- easy correction of game drift (e.g., two games played for a single team in a week)
- collect stats on teras and megas
- inline common queries into the base database as views
- channel filtering for replay collection
- move/damage tracking:
- delayed damage (Future Sight and co)
- self-damage misattribution (Belly Drum, possibly Explosion)
- indirect status (Toxic Spikes)
- weather?

25
bot.py
View File

@ -73,7 +73,7 @@ class BotClient(discord.Client):
async def on_command(self, message: discord.Message):
match re.split(" +", message.content):
case ["%calc?"] | ["%calc", "?"]:
case ["%calc?"], ["%calc", "?"]:
await message.reply(content=self._help_calculate())
case ["%calc", *args]:
try:
@ -85,27 +85,22 @@ class BotClient(discord.Client):
content="```\n" + str(e) + "\n```", delete_after=10
)
case _:
_log.info(f"Unrecognised command {message.content}")
await message.reply(content="Unrecognised command.")
_log.info(f"Unrecognised command {command}")
def _help_calculate(self) -> str:
return (
"Run Showdown damage calculations.\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"
"Supported: attacker/defender boosts, EVs, tera, item, ability, species; weather/terrain; screens."
"Not supported (popular): non-default multi hits; hazards."
)
async def _help_calculate(self) -> str:
return """
Run Showdown damage calculations.
Example format:
` -2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen`
Supported: attacker/defender boosts, EVs, item, species; attacker ability; weather/terrain; screens.
"""
async def _calculate(self, args: list[str]) -> str:
proc = sp.run(
["node", "calc_main.js", "--"] + args, stdout=sp.PIPE, stderr=sp.PIPE
)
stderr = proc.stderr.decode().strip()
if proc.returncode != 0:
raise Exception(stderr)
if stderr:
_log.warning(f"running calculation '{args}': {stderr}")
raise Exception(proc.stderr.decode())
return proc.stdout.decode()
def is_replay(self, message: discord.Message) -> bool:

84
calc.js
View File

@ -12,14 +12,6 @@ import { createToken, Lexer } from "chevrotain";
const gen = Generations.get(9);
/**
* @param {string} text
* @return {string}
*/
function escapeRegExp(text) {
return text.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
}
/**
* Creates a lexer.
*
@ -35,39 +27,20 @@ function buildLexer() {
const Item = createToken({
name: "Item",
pattern: new RegExp(
[...gen.items].map((i) => escapeRegExp(i.name)).join("|")
),
pattern: new RegExp([...gen.items].map((i) => i.name).join("|")),
});
const Ability = createToken({
name: "Ability",
pattern: new RegExp(
[...gen.abilities].map((a) => escapeRegExp(a.name)).join("|")
),
pattern: new RegExp([...gen.abilities].map((a) => a.name).join("|")),
});
const Pokemon = createToken({
name: "Pokemon",
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("|")
),
pattern: new RegExp([...gen.species].map((s) => s.name).join("|")),
});
const Move = createToken({
name: "Move",
pattern: new RegExp(
[...gen.moves]
.map((m) => escapeRegExp(m.name))
.sort((a, b) => b.length - a.length)
.join("|")
),
pattern: new RegExp([...gen.moves].map((m) => m.name).join("|")),
});
const whitespace = createToken({
@ -85,19 +58,6 @@ function buildLexer() {
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({
name: "terrainEnter",
pattern: "in",
@ -106,12 +66,10 @@ function buildLexer() {
const Weather = createToken({
name: "Weather",
pattern: /Sun|Rain|Sand|Snow/,
pop_mode: true,
});
const Terrain = createToken({
name: "Terrain",
pattern: /(Electric|Grassy|Misty|Psychic) Terrain/,
pop_mode: true,
});
const screenEnter = createToken({
@ -133,12 +91,10 @@ function buildLexer() {
Ability,
Pokemon,
Move,
teraEnter,
terrainEnter,
screenEnter,
],
tera_mode: [whitespace, Tera],
terrain_mode: [whitespace, Weather, Terrain],
terrain_mode: [whitespace, screenEnter, Weather, Terrain],
screen_mode: [whitespace, Move],
},
defaultMode: "default_mode",
@ -165,7 +121,7 @@ function* iterate(arr) {
function unwrapToken(type, token) {
assert(
token.tokenType.name == type,
`expected token ${type}, got ${token.tokenType.name}`
`expected token ${type}, got ${token.tokenType.name}`,
);
return token.image;
}
@ -255,6 +211,8 @@ function parseAndCalculate(line) {
}
}
break;
case "Stat":
throw Error("Impossible state: bare Stat");
case "Item":
opts().item = unwrapToken("Item", item);
break;
@ -271,9 +229,6 @@ function parseAndCalculate(line) {
case "Move":
move = unwrapToken("Move", item);
break;
case "teraEnter":
opts().teraType = unwrapToken("Tera", it.next().value);
break;
case "terrainEnter":
{
let next = it.next().value;
@ -282,11 +237,13 @@ function parseAndCalculate(line) {
field.weather = next.image;
break;
case "Terrain":
field.terrain = next.image.replace(" Terrain", "");
field.terrain = next.image;
break;
default:
throw Error(
`Unhandled field condition ${next.tokenType.name}: ${next.image}`
"Unhandled terrain %s: %s",
next.tokenType.name,
next.image
);
}
}
@ -329,7 +286,7 @@ function parseAndCalculate(line) {
function test() {
const text =
"-2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen";
var res = parseAndCalculate(text);
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");
@ -344,21 +301,6 @@ function test() {
res.desc().replace(/:.*/, "") === text,
"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");
res = parseAndCalculate("Ditto Thunder Punch vs. Avalugg");
assert(
res.move.name === "Thunder Punch",
"should match entire move that is a superset of another move"
);
}
export { parseAndCalculate, test };