Compare commits
	
		
			19 Commits
		
	
	
		
			c6249b1b9b
			...
			dont-resol
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						d9d8151c74
	
				 | 
					
					
						|||
| 
						
						
							
						
						5f5603fff7
	
				 | 
					
					
						|||
| 
						
						
							
						
						ce70a9fb4d
	
				 | 
					
					
						|||
| 
						
						
							
						
						ea2285ed1d
	
				 | 
					
					
						|||
| 
						
						
							
						
						6c7d61f916
	
				 | 
					
					
						|||
| 
						
						
							
						
						2acbf9945c
	
				 | 
					
					
						|||
| 
						
						
							
						
						6aa1cac328
	
				 | 
					
					
						|||
| 
						
						
							
						
						0b163e8ffd
	
				 | 
					
					
						|||
| 
						
						
							
						
						01cf81f930
	
				 | 
					
					
						|||
| 
						
						
							
						
						4692ffa259
	
				 | 
					
					
						|||
| 
						
						
							
						
						53ece70381
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa96d88964
	
				 | 
					
					
						|||
| 
						
						
							
						
						cf3906be94
	
				 | 
					
					
						|||
| 
						
						
							
						
						64cf863437
	
				 | 
					
					
						|||
| 
						
						
							
						
						c239aadfaf
	
				 | 
					
					
						|||
| 
						
						
							
						
						c75258a868
	
				 | 
					
					
						|||
| 
						
						
							
						
						70141b162f
	
				 | 
					
					
						|||
| 
						
						
							
						
						6d92cb4deb
	
				 | 
					
					
						|||
| 
						
						
							
						
						32e142b8cb
	
				 | 
					
					
						
							
								
								
									
										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?
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								bot.py
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								bot.py
									
									
									
									
									
								
							@@ -72,24 +72,40 @@ class BotClient(discord.Client):
 | 
				
			|||||||
        return message.content.startswith("%")
 | 
					        return message.content.startswith("%")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def on_command(self, message: discord.Message):
 | 
					    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]:
 | 
					            case ["%calc", *args]:
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    calc = await self._calculate(args)
 | 
					                    calc = await self._calculate(args)
 | 
				
			||||||
                    await message.reply(content=calc.strip())
 | 
					                    await message.reply(content=calc.strip())
 | 
				
			||||||
                except Exception as e:
 | 
					                except Exception as e:
 | 
				
			||||||
 | 
					                    _log.exception("running calculation")
 | 
				
			||||||
                    await message.reply(
 | 
					                    await message.reply(
 | 
				
			||||||
                        content="```\n" + str(e) + "\n```", delete_after=30
 | 
					                        content="```\n" + str(e) + "\n```", delete_after=10
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
            case _:
 | 
					            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:
 | 
					    async def _calculate(self, args: list[str]) -> str:
 | 
				
			||||||
        proc = sp.run(
 | 
					        proc = sp.run(
 | 
				
			||||||
            ["node", "calc_main.js", "--"] + args, stdout=sp.PIPE, stderr=sp.PIPE
 | 
					            ["node", "calc_main.js", "--"] + args, stdout=sp.PIPE, stderr=sp.PIPE
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        stderr = proc.stderr.decode().strip()
 | 
				
			||||||
        if proc.returncode != 0:
 | 
					        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()
 | 
					        return proc.stdout.decode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_replay(self, message: discord.Message) -> bool:
 | 
					    def is_replay(self, message: discord.Message) -> bool:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										141
									
								
								calc.js
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								calc.js
									
									
									
									
									
								
							@@ -1,12 +1,24 @@
 | 
				
			|||||||
import calc, {
 | 
					import {
 | 
				
			||||||
  calculate,
 | 
					  calculate,
 | 
				
			||||||
  Generations,
 | 
					  toID,
 | 
				
			||||||
  Field,
 | 
					  Field,
 | 
				
			||||||
 | 
					  Generations,
 | 
				
			||||||
  Move,
 | 
					  Move,
 | 
				
			||||||
  Pokemon,
 | 
					  Pokemon,
 | 
				
			||||||
 | 
					  Result,
 | 
				
			||||||
} from "@ajhyndman/smogon-calc";
 | 
					} from "@ajhyndman/smogon-calc";
 | 
				
			||||||
import assert from "assert";
 | 
					import assert from "assert";
 | 
				
			||||||
import chev, { Lexer } from "chevrotain";
 | 
					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.
 | 
					 * Creates a lexer.
 | 
				
			||||||
@@ -14,65 +26,95 @@ import chev, { Lexer } from "chevrotain";
 | 
				
			|||||||
 * @returns {Lexer}
 | 
					 * @returns {Lexer}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function buildLexer() {
 | 
					function buildLexer() {
 | 
				
			||||||
  const Boost = chev.createToken({ name: "Boost", pattern: /[+-]\d+/ });
 | 
					  const Boost = createToken({ name: "Boost", pattern: /[+-]\d+/ });
 | 
				
			||||||
  const EV = chev.createToken({ name: "EV", pattern: /\d+[+-]?/ });
 | 
					  const EV = createToken({ name: "EV", pattern: /\d+[+-]?/ });
 | 
				
			||||||
  const Stat = chev.createToken({
 | 
					  const Stat = createToken({
 | 
				
			||||||
    name: "Stat",
 | 
					    name: "Stat",
 | 
				
			||||||
    pattern: /HP|Atk|Def|SpA|SpD|Spe/,
 | 
					    pattern: /HP|Atk|Def|SpA|SpD|Spe/,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const Item = chev.createToken({
 | 
					  const Item = createToken({
 | 
				
			||||||
    name: "Item",
 | 
					    name: "Item",
 | 
				
			||||||
    pattern: new RegExp(calc.ITEMS.flatMap((gen) => gen).join("|")),
 | 
					    pattern: new RegExp(
 | 
				
			||||||
 | 
					      [...gen.items].map((i) => escapeRegExp(i.name)).join("|")
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const Ability = chev.createToken({
 | 
					  const Ability = createToken({
 | 
				
			||||||
    name: "Ability",
 | 
					    name: "Ability",
 | 
				
			||||||
    pattern: new RegExp(calc.ABILITIES.flatMap((gen) => gen).join("|")),
 | 
					    pattern: new RegExp(
 | 
				
			||||||
 | 
					      [...gen.abilities].map((a) => escapeRegExp(a.name)).join("|")
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const Pokemon = chev.createToken({
 | 
					  const Pokemon = createToken({
 | 
				
			||||||
    name: "Pokemon",
 | 
					    name: "Pokemon",
 | 
				
			||||||
    pattern: new RegExp(
 | 
					    pattern: new RegExp(
 | 
				
			||||||
      calc.SPECIES.flatMap((gen) => Object.keys(gen)).join("|")
 | 
					      [...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 = chev.createToken({
 | 
					  const Move = createToken({
 | 
				
			||||||
    name: "Move",
 | 
					    name: "Move",
 | 
				
			||||||
    pattern: new RegExp(
 | 
					    pattern: new RegExp(
 | 
				
			||||||
      calc.MOVES.flatMap((gen) => Object.keys(gen)).join("|")
 | 
					      [...gen.moves]
 | 
				
			||||||
 | 
					        .map((m) => escapeRegExp(m.name))
 | 
				
			||||||
 | 
					        .sort((a, b) => b.length - a.length)
 | 
				
			||||||
 | 
					        .join("|")
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const whitespace = chev.createToken({
 | 
					  const whitespace = createToken({
 | 
				
			||||||
    name: "Whitespace",
 | 
					    name: "Whitespace",
 | 
				
			||||||
    pattern: /\s+/,
 | 
					    pattern: /\s+/,
 | 
				
			||||||
    group: Lexer.SKIPPED,
 | 
					    group: Lexer.SKIPPED,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const div = chev.createToken({
 | 
					  const div = createToken({
 | 
				
			||||||
    name: "div",
 | 
					    name: "div",
 | 
				
			||||||
    pattern: "/",
 | 
					    pattern: "/",
 | 
				
			||||||
    group: Lexer.SKIPPED,
 | 
					    group: Lexer.SKIPPED,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const vs = chev.createToken({
 | 
					  const vs = createToken({
 | 
				
			||||||
    name: "vs",
 | 
					    name: "vs",
 | 
				
			||||||
    pattern: /vs\.?/,
 | 
					    pattern: /vs\.?/,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const terrainEnter = chev.createToken({
 | 
					  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",
 | 
					    name: "terrainEnter",
 | 
				
			||||||
    pattern: "in",
 | 
					    pattern: "in",
 | 
				
			||||||
    push_mode: "terrain_mode",
 | 
					    push_mode: "terrain_mode",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const Weather = chev.createToken({
 | 
					  const Weather = createToken({
 | 
				
			||||||
    name: "Weather",
 | 
					    name: "Weather",
 | 
				
			||||||
    pattern: /Sun|Rain|Sand|Snow/,
 | 
					    pattern: /Sun|Rain|Sand|Snow/,
 | 
				
			||||||
 | 
					    pop_mode: true,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const Terrain = chev.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 = chev.createToken({
 | 
					  const screenEnter = createToken({
 | 
				
			||||||
    name: "screenEnter",
 | 
					    name: "screenEnter",
 | 
				
			||||||
    pattern: "through",
 | 
					    pattern: "through",
 | 
				
			||||||
    push_mode: "screen_mode",
 | 
					    push_mode: "screen_mode",
 | 
				
			||||||
@@ -91,10 +133,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",
 | 
				
			||||||
@@ -115,15 +159,13 @@ function* iterate(arr) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {string} type token type
 | 
					 * @param {string} type token type
 | 
				
			||||||
 * @param {chev.IToken} token token
 | 
					 * @param {import("chevrotain").IToken} token token
 | 
				
			||||||
 * @return {string} matched token
 | 
					 * @return {string} matched token
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function unwrapToken(type, token) {
 | 
					function unwrapToken(type, token) {
 | 
				
			||||||
  assert(
 | 
					  assert(
 | 
				
			||||||
    token.tokenType.name == type,
 | 
					    token.tokenType.name == type,
 | 
				
			||||||
    "expected token %s, got %s",
 | 
					    `expected token ${type}, got ${token.tokenType.name}`
 | 
				
			||||||
    type,
 | 
					 | 
				
			||||||
    token.tokenType.name
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return token.image;
 | 
					  return token.image;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -153,7 +195,7 @@ const NEG_NATURES = {
 | 
				
			|||||||
 * through Light Screen
 | 
					 * through Light Screen
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {string} line textual line
 | 
					 * @param {string} line textual line
 | 
				
			||||||
 * @return {calc.Result} calculation result
 | 
					 * @return {Result} calculation result
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parseAndCalculate(line) {
 | 
					function parseAndCalculate(line) {
 | 
				
			||||||
  const lexer = buildLexer();
 | 
					  const lexer = buildLexer();
 | 
				
			||||||
@@ -167,14 +209,14 @@ function parseAndCalculate(line) {
 | 
				
			|||||||
  /** @type {string} */
 | 
					  /** @type {string} */
 | 
				
			||||||
  var defender;
 | 
					  var defender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** @type {calc.State.Pokemon} */
 | 
					  /** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */
 | 
				
			||||||
  var attackerOpts = {};
 | 
					  var attackerOpts = {};
 | 
				
			||||||
  /** @type {calc.State.Pokemon} */
 | 
					  /** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */
 | 
				
			||||||
  var defenderOpts = {};
 | 
					  var defenderOpts = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /** @type {string} */
 | 
					  /** @type {string} */
 | 
				
			||||||
  var move;
 | 
					  var move;
 | 
				
			||||||
  /** @type {calc.State.Field} */
 | 
					  /** @type {import("@ajhyndman/smogon-calc").State.Field} */
 | 
				
			||||||
  var field = {};
 | 
					  var field = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Tokenising state.
 | 
					  // Tokenising state.
 | 
				
			||||||
@@ -213,11 +255,12 @@ function parseAndCalculate(line) {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case "Stat":
 | 
					 | 
				
			||||||
        throw Error("Impossibel state: bare Stat");
 | 
					 | 
				
			||||||
      case "Item":
 | 
					      case "Item":
 | 
				
			||||||
        opts().item = unwrapToken("Item", item);
 | 
					        opts().item = unwrapToken("Item", item);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case "Ability":
 | 
				
			||||||
 | 
					        opts().ability = unwrapToken("Ability", item);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      case "Pokemon":
 | 
					      case "Pokemon":
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          let name = unwrapToken("Pokemon", item);
 | 
					          let name = unwrapToken("Pokemon", item);
 | 
				
			||||||
@@ -228,6 +271,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;
 | 
				
			||||||
@@ -236,13 +282,11 @@ function parseAndCalculate(line) {
 | 
				
			|||||||
              field.weather = next.image;
 | 
					              field.weather = next.image;
 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            case "Terrain":
 | 
					            case "Terrain":
 | 
				
			||||||
              field.terrain = next.image;
 | 
					              field.terrain = next.image.replace(" Terrain", "");
 | 
				
			||||||
              break;
 | 
					              break;
 | 
				
			||||||
            default:
 | 
					            default:
 | 
				
			||||||
              throw Error(
 | 
					              throw Error(
 | 
				
			||||||
                "Unhandled terrain %s: %s",
 | 
					                `Unhandled field condition ${next.tokenType.name}: ${next.image}`
 | 
				
			||||||
                next.tokenType.name,
 | 
					 | 
				
			||||||
                next.image
 | 
					 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -266,7 +310,13 @@ function parseAndCalculate(line) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const gen = Generations.get(8);
 | 
					  // 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(
 | 
					  return calculate(
 | 
				
			||||||
    gen,
 | 
					    gen,
 | 
				
			||||||
    new Pokemon(gen, attacker, attackerOpts),
 | 
					    new Pokemon(gen, attacker, attackerOpts),
 | 
				
			||||||
@@ -279,7 +329,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");
 | 
				
			||||||
@@ -294,6 +344,21 @@ 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");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 };
 | 
					export { parseAndCalculate, test };
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user