Compare commits
12 Commits
cf3906be94
...
dont-resol
Author | SHA1 | Date | |
---|---|---|---|
d9d8151c74
|
|||
5f5603fff7
|
|||
ce70a9fb4d
|
|||
ea2285ed1d
|
|||
6c7d61f916
|
|||
2acbf9945c
|
|||
6aa1cac328
|
|||
0b163e8ffd
|
|||
01cf81f930
|
|||
4692ffa259
|
|||
53ece70381
|
|||
fa96d88964
|
10
README.md
10
README.md
@ -43,3 +43,13 @@ 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?
|
||||
|
23
bot.py
23
bot.py
@ -72,7 +72,9 @@ class BotClient(discord.Client):
|
||||
return message.content.startswith("%")
|
||||
|
||||
async def on_command(self, message: discord.Message):
|
||||
match message.content.split(" "):
|
||||
match re.split(" +", message.content):
|
||||
case ["%calc?"] | ["%calc", "?"]:
|
||||
await message.reply(content=self._help_calculate())
|
||||
case ["%calc", *args]:
|
||||
try:
|
||||
calc = await self._calculate(args)
|
||||
@ -80,17 +82,30 @@ class BotClient(discord.Client):
|
||||
except Exception as e:
|
||||
_log.exception("running calculation")
|
||||
await message.reply(
|
||||
content="```\n" + str(e) + "\n```", delete_after=30
|
||||
content="```\n" + str(e) + "\n```", delete_after=10
|
||||
)
|
||||
case _:
|
||||
_log.info(f"Unrecognised command {command}")
|
||||
_log.info(f"Unrecognised command {message.content}")
|
||||
await message.reply(content="Unrecognised 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 _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(proc.stderr.decode())
|
||||
raise Exception(stderr)
|
||||
if stderr:
|
||||
_log.warning(f"running calculation '{args}': {stderr}")
|
||||
return proc.stdout.decode()
|
||||
|
||||
def is_replay(self, message: discord.Message) -> bool:
|
||||
|
84
calc.js
84
calc.js
@ -12,6 +12,14 @@ 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.
|
||||
*
|
||||
@ -27,20 +35,39 @@ function buildLexer() {
|
||||
|
||||
const Item = createToken({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
name: "Move",
|
||||
pattern: new RegExp([...gen.moves].map((m) => m.name).join("|")),
|
||||
pattern: new RegExp(
|
||||
[...gen.moves]
|
||||
.map((m) => escapeRegExp(m.name))
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.join("|")
|
||||
),
|
||||
});
|
||||
|
||||
const whitespace = createToken({
|
||||
@ -58,6 +85,19 @@ 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",
|
||||
@ -66,10 +106,12 @@ 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({
|
||||
@ -91,10 +133,12 @@ function buildLexer() {
|
||||
Ability,
|
||||
Pokemon,
|
||||
Move,
|
||||
teraEnter,
|
||||
terrainEnter,
|
||||
screenEnter,
|
||||
],
|
||||
terrain_mode: [whitespace, screenEnter, Weather, Terrain],
|
||||
tera_mode: [whitespace, Tera],
|
||||
terrain_mode: [whitespace, Weather, Terrain],
|
||||
screen_mode: [whitespace, Move],
|
||||
},
|
||||
defaultMode: "default_mode",
|
||||
@ -121,7 +165,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;
|
||||
}
|
||||
@ -211,8 +255,6 @@ function parseAndCalculate(line) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Stat":
|
||||
throw Error("Impossible state: bare Stat");
|
||||
case "Item":
|
||||
opts().item = unwrapToken("Item", item);
|
||||
break;
|
||||
@ -229,6 +271,9 @@ 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;
|
||||
@ -237,13 +282,11 @@ function parseAndCalculate(line) {
|
||||
field.weather = next.image;
|
||||
break;
|
||||
case "Terrain":
|
||||
field.terrain = next.image;
|
||||
field.terrain = next.image.replace(" Terrain", "");
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
"Unhandled terrain %s: %s",
|
||||
next.tokenType.name,
|
||||
next.image
|
||||
`Unhandled field condition ${next.tokenType.name}: ${next.image}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -286,7 +329,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";
|
||||
const res = parseAndCalculate(text);
|
||||
var res = parseAndCalculate(text);
|
||||
|
||||
assert(res.attacker.boosts.spa === -2, "should have -2 SpA");
|
||||
assert(res.attacker.evs.spa === 8, "should have 8 SpA EVs");
|
||||
@ -301,6 +344,21 @@ 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 };
|
||||
|
Reference in New Issue
Block a user