Compare commits
30 Commits
bf1793e561
...
dont-resol
Author | SHA1 | Date | |
---|---|---|---|
d9d8151c74
|
|||
5f5603fff7
|
|||
ce70a9fb4d
|
|||
ea2285ed1d
|
|||
6c7d61f916
|
|||
2acbf9945c
|
|||
6aa1cac328
|
|||
0b163e8ffd
|
|||
01cf81f930
|
|||
4692ffa259
|
|||
53ece70381
|
|||
fa96d88964
|
|||
cf3906be94
|
|||
64cf863437
|
|||
c239aadfaf
|
|||
c75258a868
|
|||
70141b162f
|
|||
6d92cb4deb
|
|||
32e142b8cb
|
|||
c6249b1b9b
|
|||
4f1e84ba8e
|
|||
225cac02c0
|
|||
a1f2f175cd
|
|||
4b07aeb10c
|
|||
699a833285
|
|||
648deb83db
|
|||
a926461f8f
|
|||
a65137307b
|
|||
0afca583b0
|
|||
dc668c67a0
|
145
.gitignore
vendored
145
.gitignore
vendored
@ -11,3 +11,148 @@ htmlcov
|
|||||||
*.db
|
*.db
|
||||||
cache/
|
cache/
|
||||||
token
|
token
|
||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
13
README.md
13
README.md
@ -3,6 +3,8 @@
|
|||||||
Pokemon Showdown data processing, mostly for HHIRLLL's Pokemon league. Ugly as
|
Pokemon Showdown data processing, mostly for HHIRLLL's Pokemon league. Ugly as
|
||||||
fuck.
|
fuck.
|
||||||
|
|
||||||
|
Also includes a bot to automate replay ingestion and provide misc utilities.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python with sqlite
|
- Python with sqlite
|
||||||
@ -40,3 +42,14 @@ whatever SQL queries against the data file (default `data.db`) you want.
|
|||||||
- include timestamps in logs to correlate KOs with the mon that KOed
|
- include timestamps in logs to correlate KOs with the mon that KOed
|
||||||
- calculate gametime based on active turns rather than moves used
|
- calculate gametime based on active turns rather than moves used
|
||||||
- also solves the issue where paralyzed/confused turns are not counted
|
- 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?
|
||||||
|
44
bot.py
44
bot.py
@ -61,11 +61,53 @@ class BotClient(discord.Client):
|
|||||||
|
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
content = message.content
|
content = message.content
|
||||||
if self.is_replay(message):
|
if self.is_command(message):
|
||||||
|
await self.on_command(message)
|
||||||
|
elif self.is_replay(message):
|
||||||
await self.on_replay(message)
|
await self.on_replay(message)
|
||||||
elif self.is_leaguefact(message):
|
elif self.is_leaguefact(message):
|
||||||
await self.on_leaguefact(message)
|
await self.on_leaguefact(message)
|
||||||
|
|
||||||
|
def is_command(self, message: discord.Message) -> bool:
|
||||||
|
return message.content.startswith("%")
|
||||||
|
|
||||||
|
async def on_command(self, message: discord.Message):
|
||||||
|
match re.split(" +", message.content):
|
||||||
|
case ["%calc?"] | ["%calc", "?"]:
|
||||||
|
await message.reply(content=self._help_calculate())
|
||||||
|
case ["%calc", *args]:
|
||||||
|
try:
|
||||||
|
calc = await self._calculate(args)
|
||||||
|
await message.reply(content=calc.strip())
|
||||||
|
except Exception as e:
|
||||||
|
_log.exception("running calculation")
|
||||||
|
await message.reply(
|
||||||
|
content="```\n" + str(e) + "\n```", delete_after=10
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
_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(stderr)
|
||||||
|
if stderr:
|
||||||
|
_log.warning(f"running calculation '{args}': {stderr}")
|
||||||
|
return proc.stdout.decode()
|
||||||
|
|
||||||
def is_replay(self, message: discord.Message) -> bool:
|
def is_replay(self, message: discord.Message) -> bool:
|
||||||
if re.match("https://replay.pokemonshowdown.com/dl-.*", message.content):
|
if re.match("https://replay.pokemonshowdown.com/dl-.*", message.content):
|
||||||
return True
|
return True
|
||||||
|
364
calc.js
Normal file
364
calc.js
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function escapeRegExp(text) {
|
||||||
|
return text.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => escapeRegExp(i.name)).join("|")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const Ability = createToken({
|
||||||
|
name: "Ability",
|
||||||
|
pattern: new RegExp(
|
||||||
|
[...gen.abilities].map((a) => escapeRegExp(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("|")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const Move = createToken({
|
||||||
|
name: "Move",
|
||||||
|
pattern: new RegExp(
|
||||||
|
[...gen.moves]
|
||||||
|
.map((m) => escapeRegExp(m.name))
|
||||||
|
.sort((a, b) => b.length - a.length)
|
||||||
|
.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 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",
|
||||||
|
push_mode: "terrain_mode",
|
||||||
|
});
|
||||||
|
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({
|
||||||
|
name: "screenEnter",
|
||||||
|
pattern: "through",
|
||||||
|
push_mode: "screen_mode",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Lexer({
|
||||||
|
modes: {
|
||||||
|
default_mode: [
|
||||||
|
whitespace,
|
||||||
|
div,
|
||||||
|
vs,
|
||||||
|
Boost,
|
||||||
|
EV,
|
||||||
|
Stat,
|
||||||
|
Item,
|
||||||
|
Ability,
|
||||||
|
Pokemon,
|
||||||
|
Move,
|
||||||
|
teraEnter,
|
||||||
|
terrainEnter,
|
||||||
|
screenEnter,
|
||||||
|
],
|
||||||
|
tera_mode: [whitespace, Tera],
|
||||||
|
terrain_mode: [whitespace, 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 "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 "teraEnter":
|
||||||
|
opts().teraType = unwrapToken("Tera", it.next().value);
|
||||||
|
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.replace(" Terrain", "");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Error(
|
||||||
|
`Unhandled field condition ${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";
|
||||||
|
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");
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
|
||||||
|
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 };
|
8
calc_main.js
Normal file
8
calc_main.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { parseAndCalculate } from "./calc.js";
|
||||||
|
|
||||||
|
// FIXME: issue with using execArgv.
|
||||||
|
let args = process.argv.slice(2);
|
||||||
|
if (args[0] === "--") args = args.slice(1);
|
||||||
|
const line = args.join(" ");
|
||||||
|
const res = parseAndCalculate(line);
|
||||||
|
console.log(res.fullDesc());
|
@ -17,7 +17,11 @@
|
|||||||
ps.requests
|
ps.requests
|
||||||
]);
|
]);
|
||||||
in
|
in
|
||||||
[ python pkgs.sqlite ];
|
[
|
||||||
|
pkgs.nodejs
|
||||||
|
python
|
||||||
|
pkgs.sqlite
|
||||||
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
82
package-lock.json
generated
Normal file
82
package-lock.json
generated
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "hhirls",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "hhirls",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"dependencies": {
|
||||||
|
"@ajhyndman/smogon-calc": "^0.8.0",
|
||||||
|
"chevrotain": "^10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ajhyndman/smogon-calc": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ajhyndman/smogon-calc/-/smogon-calc-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-jx/gY1uSD70skzE0xE7+lUPp7KxKKqj3PcyRLMCejbd9A68PwnBKfbDP3pW5ILX9Dy+9R9m6OkiPCa9K4qi/pQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^18.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/cst-dts-gen": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/gast": "10.5.0",
|
||||||
|
"@chevrotain/types": "10.5.0",
|
||||||
|
"lodash": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/gast": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/types": "10.5.0",
|
||||||
|
"lodash": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/types": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/utils": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz",
|
||||||
|
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
|
||||||
|
},
|
||||||
|
"node_modules/chevrotain": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/cst-dts-gen": "10.5.0",
|
||||||
|
"@chevrotain/gast": "10.5.0",
|
||||||
|
"@chevrotain/types": "10.5.0",
|
||||||
|
"@chevrotain/utils": "10.5.0",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"regexp-to-ast": "0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
|
"node_modules/regexp-to-ast": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "hhirls",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Pokemon Showdown data processing, mostly for HHIRLLL's Pokemon league. Ugly as fuck.",
|
||||||
|
"main": "calc.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"calc": "node calc_main.js",
|
||||||
|
"test": "node -e 'import(\"./calc.js\").then(mod => mod.test())'"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"dependencies": {
|
||||||
|
"@ajhyndman/smogon-calc": "^0.8.0",
|
||||||
|
"chevrotain": "^10.5.0"
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user