From 72d9c6915271b0b122db2bfcd4152e7fa8e41d7b Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:15:28 -0500 Subject: [PATCH 01/39] gen 1/2 moves/abilities manual data json overrides --- src/poke_env/data/gen_data.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/poke_env/data/gen_data.py b/src/poke_env/data/gen_data.py index 9f8c3cc10..c43833f82 100644 --- a/src/poke_env/data/gen_data.py +++ b/src/poke_env/data/gen_data.py @@ -34,7 +34,15 @@ def load_moves(self, gen: int) -> Dict[str, Any]: with open( os.path.join(self._static_files_root, "moves", f"gen{gen}moves.json") ) as f: - return orjson.loads(f.read()) + data = orjson.loads(f.read()) + + # manually fix data entries gathered from Showdown data files + if gen == 1: + data["recover"]["heal"] = [1, 2] + data["softboiled"]["heal"] = [1, 2] + # TODO: check vicegrip / visegrip + + return data def load_natures(self) -> Dict[str, Dict[str, Union[int, float]]]: with open(os.path.join(self._static_files_root, "natures.json")) as f: @@ -64,6 +72,10 @@ def load_pokedex(self, gen: int) -> Dict[str, Any]: dex.update(other_forms_dex) for name, value in dex.items(): + if gen <= 2 and "abilities" in value: + # remove abilities from gen 1-2 + # JAKE: check if this is still the way to handle this + value["abilities"] = {"0": "No Ability"} if "baseSpecies" in value: value["species"] = value["baseSpecies"] else: From dd9a03d19aaa0b450148ee1e6ba983f4e9962dcd Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:30:04 -0500 Subject: [PATCH 02/39] re-add Pokemon previous move --- src/poke_env/environment/pokemon.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index 35387fa36..7d2a8de9a 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -39,6 +39,7 @@ class Pokemon: "_possible_abilities", "_preparing_move", "_preparing_target", + "_previous_move", "_protect_counter", "_shiny", "_stats", @@ -109,6 +110,7 @@ def __init__( self._must_recharge: bool = False self._preparing_move: Optional[Move] = None self._preparing_target: Optional[bool | Pokemon] = None + self._previous_move: Optional[Move] = None self._protect_counter: int = 0 self._revealed: bool = False self._stats: Dict[str, Optional[int]] = { @@ -298,6 +300,7 @@ def moved(self, move_id: str, failed: bool = False, use: bool = True): self._preparing_move = None self._preparing_target = None move = self._add_move(move_id, use=use) + self._previous_move = move if move and move.is_protect_counter and not failed: self._protect_counter += 1 @@ -343,6 +346,10 @@ def prepare(self, move_id: str, target: Optional[Pokemon]): self._preparing_move = move self._preparing_target = target + @property + def previous_move(self) -> Optional[Move]: + return self._previous_move + def primal(self): species_id_str = to_id_str(self._species) primal_species = ( From f244a28b04c9651801007fa90528fb84ef1916c1 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:33:53 -0500 Subject: [PATCH 03/39] pokemon first_turn False after moved --- src/poke_env/environment/pokemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index 7d2a8de9a..499704bdb 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -301,6 +301,7 @@ def moved(self, move_id: str, failed: bool = False, use: bool = True): self._preparing_target = None move = self._add_move(move_id, use=use) self._previous_move = move + self._first_turn = False if move and move.is_protect_counter and not failed: self._protect_counter += 1 From f0ae238573d37554369229d4d6c22b202f181a0e Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:36:53 -0500 Subject: [PATCH 04/39] additional AbstractBattle ignores --- src/poke_env/environment/abstract_battle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 9b7f2c259..b5fecdb55 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -37,6 +37,7 @@ class AbstractBattle(ABC): "J", "L", "askreg", + "badge", "c", "chat", "crit", @@ -53,7 +54,9 @@ class AbstractBattle(ABC): "leave", "n", "name", + "noinit", "rated", + "rename", "resisted", "sentchoice", "split", From 86aeb8d1bd491a6e3682dd26a3c5135df860bd2d Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:41:19 -0500 Subject: [PATCH 05/39] route '-activate ability:' messages away from adding 'effects' --- src/poke_env/environment/abstract_battle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index b5fecdb55..9c874686d 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -638,7 +638,10 @@ def parse_message(self, split_message: List[str]): self.opponent_can_dynamax = False elif event[1] == "-activate": target, effect = event[2:4] - if target and effect == "move: Skill Swap": + if effect.startswith("ability: "): + ability = effect[9:] + self.get_pokemon(target).ability = ability + elif target and effect == "move: Skill Swap": self.get_pokemon(target).start_effect(effect, event[4:6]) actor = event[6].replace("[of] ", "") self.get_pokemon(actor).set_temporary_ability(event[5]) From 4383e416a1f000f6115fa0630fbfa183579a8d24 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:45:26 -0500 Subject: [PATCH 06/39] add note to check guardwap swapboost later; protect against missing condition when clearing SideConditions --- src/poke_env/environment/abstract_battle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 9c874686d..49662dc73 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -787,6 +787,8 @@ def parse_message(self, split_message: List[str]): source, target, stats = event[2:5] source_mon = self.get_pokemon(source) target_mon = self.get_pokemon(target) + # JAKE: I have a manual patch for guardswap in the metamon version. + # TODO: check guardswap. for stat in stats.split(", "): source_mon.boosts[stat], target_mon.boosts[stat] = ( target_mon.boosts[stat], @@ -930,7 +932,7 @@ def side_end(self, side: str, condition_str: str): else: conditions = self.opponent_side_conditions condition = SideCondition.from_showdown_message(condition_str) - if condition is not SideCondition.UNKNOWN: + if condition is not SideCondition.UNKNOWN and condition in conditions: conditions.pop(condition) def _side_start(self, side: str, condition_str: str): From afce1019993da7e3dca35d8e0e18725d721eb8c9 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 15:53:19 -0500 Subject: [PATCH 07/39] expand Effects list; light screen occasionally shows up as an effect in early gens --- src/poke_env/environment/effect.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/effect.py b/src/poke_env/environment/effect.py index 711b3327b..59d2377f4 100644 --- a/src/poke_env/environment/effect.py +++ b/src/poke_env/environment/effect.py @@ -1,5 +1,4 @@ -"""This module defines the Effect class, which represents in-game effects. -""" +"""This module defines the Effect class, which represents in-game effects.""" from __future__ import annotations @@ -24,6 +23,7 @@ class Effect(Enum): BANEFUL_BUNKER = auto() BATTLE_BOND = auto() BEAK_BLAST = auto() + BEAT_UP = auto() BIDE = auto() BIND = auto() BURNING_BULWARK = auto() @@ -75,6 +75,7 @@ class Effect(Enum): FUTURE_SIGHT = auto() GASTRO_ACID = auto() GLAIVE_RUSH = auto() + GOOEY = auto() GRAVITY = auto() GRUDGE = auto() GUARD_SPLIT = auto() @@ -85,6 +86,7 @@ class Effect(Enum): G_MAX_RAPID_FLOW = auto() G_MAX_SANDBLAST = auto() HADRON_ENGINE = auto() + HAZE = auto() HEAL_BELL = auto() HEAL_BLOCK = auto() HEALER = auto() @@ -107,6 +109,7 @@ class Effect(Enum): LEECH_SEED = auto() LEPPA_BERRY = auto() LIGHTNING_ROD = auto() + LIGHT_SCREEN = auto() LIMBER = auto() LIQUID_OOZE = auto() LOCKED_MOVE = auto() @@ -124,8 +127,10 @@ class Effect(Enum): MIRACLE_EYE = auto() MIST = auto() MISTY_TERRAIN = auto() + MUD_SPORT = auto() MUMMY = auto() MUST_RECHARGE = auto() + MYSTERY_BERRY = auto() NEUTRALIZING_GAS = auto() NIGHTMARE = auto() NO_RETREAT = auto() @@ -169,6 +174,7 @@ class Effect(Enum): QUICK_GUARD = auto() RAGE = auto() RAGE_POWDER = auto() + RAMPAGE = auto() REFLECT = auto() RIPEN = auto() ROOST = auto() @@ -257,6 +263,8 @@ def from_showdown_message(message: str) -> Effect: if message == "FALLENUNDEFINED": message = "FALLEN" + if message == "LIGHTSCREEN": + message = "LIGHT_SCREEN" try: return Effect[message] From c2c6db16ced3b7160ee071a38baade90f705a76e Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 16:04:09 -0500 Subject: [PATCH 08/39] force Pokemon current_hp_fraction to float --- src/poke_env/environment/pokemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index 499704bdb..61885a9b1 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -778,8 +778,8 @@ def current_hp_fraction(self) -> float: :rtype: float """ if self.current_hp: - return self.current_hp / self.max_hp - return 0 + return self.current_hp / float(self.max_hp) + return 0.0 @property def effects(self) -> Dict[Effect, int]: From c861c0a04dfedcd64d2b138363f95b5e7aedbb84 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 16:10:07 -0500 Subject: [PATCH 09/39] try to catch SideCondition messages with inconsistent spacing --- src/poke_env/environment/side_condition.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/side_condition.py b/src/poke_env/environment/side_condition.py index dec8c9944..94fcf1c89 100644 --- a/src/poke_env/environment/side_condition.py +++ b/src/poke_env/environment/side_condition.py @@ -51,10 +51,15 @@ def from_showdown_message(message: str): message = message.replace("move: ", "") message = message.replace(" ", "_") message = message.replace("-", "_") - + condition_code = message.upper() try: - return SideCondition[message.upper()] + return SideCondition[condition_code] except KeyError: + # hack attempt to catch conditions w/ inconsistent spacing (mainly in old replays) + # JAKE: recheck this on v0.8.3 + for known_condition in SideCondition: + if known_condition.name.replace("_", "") == condition_code: + return known_condition logging.getLogger("poke-env").warning( "Unexpected side condition '%s' received. SideCondition.UNKNOWN will be" " used instead. If this is unexpected, please open an issue at " From 80d0ff2a984c9135bbdcb61bb694be4047f451cb Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 16:33:23 -0500 Subject: [PATCH 10/39] backwards port: Leave battle room when finished --- src/poke_env/player/player.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index 302b00d81..9ec979a21 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -1,5 +1,4 @@ -"""This module defines a base class for players. -""" +"""This module defines a base class for players.""" import asyncio import random @@ -289,6 +288,8 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): self._battle_finished_callback(battle) async with self._battle_end_condition: self._battle_end_condition.notify_all() + if hasattr(self.ps_client, "websocket"): + await self.ps_client.send_message(f"/leave {battle.battle_tag}") elif split_message[1] == "error": self.logger.log( 25, "Error message received: %s", "|".join(split_message) From b889b1d0f7c99a6ad434642017562fb3c5931635 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 16:52:36 -0500 Subject: [PATCH 11/39] from future: Fix Reflect Type/Heart Swap errors; from memtamon: add guardswap edge case from old replays --- src/poke_env/environment/abstract_battle.py | 38 ++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 49662dc73..7c579fe69 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -446,7 +446,11 @@ def parse_message(self, split_message: List[str]): if event[-1].startswith("[spread]"): event = event[:-1] - if event[-1] in {"[from]lockedmove", "[from]Pursuit", "[zeffect]"}: + if event[-1].replace(" ", "") in { + "[from]lockedmove", + "[from]Pursuit", + "[zeffect]", + }: event = event[:-1] if event[-1].startswith("[anim]"): @@ -787,13 +791,31 @@ def parse_message(self, split_message: List[str]): source, target, stats = event[2:5] source_mon = self.get_pokemon(source) target_mon = self.get_pokemon(target) - # JAKE: I have a manual patch for guardswap in the metamon version. - # TODO: check guardswap. - for stat in stats.split(", "): - source_mon.boosts[stat], target_mon.boosts[stat] = ( - target_mon.boosts[stat], - source_mon.boosts[stat], - ) + if "[from]" in stats: + if "guardswap" in stats: + # JAKE: need to use metamon to check if this ever triggers + all_stats = ["def", "spd"] + else: + all_stats = [ + "accuracy", + "atk", + "def", + "evasion", + "spa", + "spd", + "spe", + ] + for stat in all_stats: + source_mon.boosts[stat], target_mon.boosts[stat] = ( + target_mon.boosts[stat], + source_mon.boosts[stat], + ) + else: + for stat in stats.split(", "): + source_mon.boosts[stat], target_mon.boosts[stat] = ( + target_mon.boosts[stat], + source_mon.boosts[stat], + ) elif event[1] == "-transform": pokemon, into = event[2:4] self.get_pokemon(pokemon).transform(self.get_pokemon(into)) From 10e7a7bda2f8325a810e65cb432dca03f2394d43 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 17:22:41 -0500 Subject: [PATCH 12/39] minor black changes to player top-level docstrings --- src/poke_env/player/__init__.py | 3 +-- src/poke_env/player/env_player.py | 3 +-- src/poke_env/player/random_player.py | 3 +-- src/poke_env/player/utils.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/poke_env/player/__init__.py b/src/poke_env/player/__init__.py index 0f88467cc..8349adf63 100644 --- a/src/poke_env/player/__init__.py +++ b/src/poke_env/player/__init__.py @@ -1,5 +1,4 @@ -"""poke_env.player module init. -""" +"""poke_env.player module init.""" from poke_env.concurrency import POKE_LOOP from poke_env.player import env_player, openai_api, player, random_player, utils diff --git a/src/poke_env/player/env_player.py b/src/poke_env/player/env_player.py index 61c0f810f..babb01b39 100644 --- a/src/poke_env/player/env_player.py +++ b/src/poke_env/player/env_player.py @@ -1,5 +1,4 @@ -"""This module defines a player class exposing the Open AI Gym API with utility functions. -""" +"""This module defines a player class exposing the Open AI Gym API with utility functions.""" from abc import ABC from threading import Lock diff --git a/src/poke_env/player/random_player.py b/src/poke_env/player/random_player.py index f7cd35c25..3055ccb9d 100644 --- a/src/poke_env/player/random_player.py +++ b/src/poke_env/player/random_player.py @@ -1,5 +1,4 @@ -"""This module defines a random players baseline -""" +"""This module defines a random players baseline""" from poke_env.environment import AbstractBattle from poke_env.player.battle_order import BattleOrder diff --git a/src/poke_env/player/utils.py b/src/poke_env/player/utils.py index e2d9dc24d..ff7c4cb36 100644 --- a/src/poke_env/player/utils.py +++ b/src/poke_env/player/utils.py @@ -1,5 +1,4 @@ -"""This module contains utility functions and objects related to Player classes. -""" +"""This module contains utility functions and objects related to Player classes.""" import asyncio import math From 6659312e7b30b2f8cee18a921da18636681b6b96 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 18:13:29 -0500 Subject: [PATCH 13/39] from past: return battle deepcopy so (last_battle, battle) reward computation is not broken --- src/poke_env/player/openai_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poke_env/player/openai_api.py b/src/poke_env/player/openai_api.py index 19270c3d5..153663523 100644 --- a/src/poke_env/player/openai_api.py +++ b/src/poke_env/player/openai_api.py @@ -350,7 +350,7 @@ def step( return obs, 0.0, False, False, info if self.current_battle.finished: raise RuntimeError("Battle is already finished, call reset") - battle = copy.copy(self.current_battle) + battle = copy.deepcopy(self.current_battle) battle.logger = None self.last_battle = battle self._actions.put(action) From f4fa97dd275635df432c88cb073e7ecb6c05d393 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 18:14:43 -0500 Subject: [PATCH 14/39] experimental: recover some of deepcopy lost time by dodging deepcopy of moves dicts --- src/poke_env/environment/move.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/move.py b/src/poke_env/environment/move.py index c1352beb8..69f2dea62 100644 --- a/src/poke_env/environment/move.py +++ b/src/poke_env/environment/move.py @@ -80,7 +80,7 @@ class Move: "_dynamaxed_move", "_gen", "_is_empty", - "_moves_dict", + "_gen_data", "_request_target", ) @@ -88,7 +88,7 @@ def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None): self._id = move_id self._base_power_override = None self._gen = gen - self._moves_dict = GenData.from_gen(gen).moves + self._gen_data = GenData.from_gen(gen) if move_id.startswith("hiddenpower") and raw_id is not None: base_power = "".join([c for c in raw_id if c.isdigit()]) @@ -106,6 +106,11 @@ def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None): self._dynamaxed_move = None self._request_target = None + @property + def _moves_dict(self) -> Dict[str, Any]: + # we'll only need the moves dict, but let GenData shallow copy + return self._gen_data.moves + def __repr__(self) -> str: return f"{self._id} (Move object)" From 5f62d89825018304b64a94cd43ded80876a9bb69 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 20:35:38 -0500 Subject: [PATCH 15/39] strip out observation system that can be done in a wrapper on the RL side --- src/poke_env/environment/__init__.py | 6 -- src/poke_env/environment/observation.py | 34 -------- src/poke_env/environment/observed_pokemon.py | 84 -------------------- 3 files changed, 124 deletions(-) delete mode 100644 src/poke_env/environment/observation.py delete mode 100644 src/poke_env/environment/observed_pokemon.py diff --git a/src/poke_env/environment/__init__.py b/src/poke_env/environment/__init__.py index 4a5b71411..fc0bbf7f3 100644 --- a/src/poke_env/environment/__init__.py +++ b/src/poke_env/environment/__init__.py @@ -6,8 +6,6 @@ field, move, move_category, - observation, - observed_pokemon, pokemon, pokemon_gender, pokemon_type, @@ -24,8 +22,6 @@ from poke_env.environment.field import Field from poke_env.environment.move import SPECIAL_MOVES, EmptyMove, Move from poke_env.environment.move_category import MoveCategory -from poke_env.environment.observation import Observation -from poke_env.environment.observed_pokemon import ObservedPokemon from poke_env.environment.pokemon import Pokemon from poke_env.environment.pokemon_gender import PokemonGender from poke_env.environment.pokemon_type import PokemonType @@ -44,8 +40,6 @@ "Field", "Move", "MoveCategory", - "Observation", - "ObservedPokemon", "Pokemon", "PokemonGender", "PokemonType", diff --git a/src/poke_env/environment/observation.py b/src/poke_env/environment/observation.py deleted file mode 100644 index 1fcf8eca4..000000000 --- a/src/poke_env/environment/observation.py +++ /dev/null @@ -1,34 +0,0 @@ -"""This module defines the Observation class, which stores the state of the battle. -It is updated whenever a new event is received and processed from showdown. Each observation -records a turn. Each property is the state of the battle at the beginning of the turn, and -the events are ones that occurred that turn. In this way, you can instanciate a new battle -with the Observations' properties, and then recreate that turn with the events property. -""" - -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union - -from poke_env.environment.field import Field -from poke_env.environment.observed_pokemon import ObservedPokemon -from poke_env.environment.side_condition import SideCondition -from poke_env.environment.weather import Weather - - -@dataclass -class Observation: - side_conditions: Dict[SideCondition, int] = field(default_factory=dict) - opponent_side_conditions: Dict[SideCondition, int] = field(default_factory=dict) - - weather: Dict[Weather, int] = field(default_factory=dict) - fields: Dict[Field, int] = field(default_factory=dict) - - active_pokemon: Union[ObservedPokemon, None, List[ObservedPokemon]] = None - opponent_active_pokemon: Union[ObservedPokemon, List[ObservedPokemon], None] = None - - # The player's team, so we can track states of mons throughout the battle - team: Dict[str, Optional[ObservedPokemon]] = field(default_factory=dict) - - # The opponent's team that has been exposed to the player, for VGC - opponent_team: Dict[str, Optional[ObservedPokemon]] = field(default_factory=dict) - - events: List[List[str]] = field(default_factory=list) diff --git a/src/poke_env/environment/observed_pokemon.py b/src/poke_env/environment/observed_pokemon.py deleted file mode 100644 index b60398e48..000000000 --- a/src/poke_env/environment/observed_pokemon.py +++ /dev/null @@ -1,84 +0,0 @@ -"""This module defines the ObservedPokmon class, which stores -what we have observed about a pokemon throughout a battle -""" - -import sys -from copy import copy -from dataclasses import dataclass, field -from typing import Dict, List, Mapping, Optional, Union - -from poke_env.environment.effect import Effect -from poke_env.environment.move import Move -from poke_env.environment.pokemon import Pokemon -from poke_env.environment.pokemon_gender import PokemonGender -from poke_env.environment.pokemon_type import PokemonType -from poke_env.environment.status import Status - - -@dataclass -class ObservedPokemon: - species: str - level: int - - ability: Optional[str] = None - boosts: Dict[str, int] = field( - default_factory=lambda: { - "accuracy": 0, - "atk": 0, - "def": 0, - "evasion": 0, - "spa": 0, - "spd": 0, - "spe": 0, - } - ) - current_hp_fraction: float = 1.0 - effects: Dict[Effect, int] = field(default_factory=dict) - is_dynamaxed: bool = False - is_terastallized: bool = False - item: Optional[str] = None - gender: Optional[PokemonGender] = None - moves: Dict[str, Move] = field(default_factory=dict) - tera_type: Optional[PokemonType] = None - shiny: Optional[bool] = None - stats: Optional[Mapping[str, Union[List[int], int, None]]] = None - status: Optional[Status] = None - - @staticmethod - def initial_stats() -> Dict[str, List[int]]: - return { - "atk": [0, sys.maxsize], - "def": [0, sys.maxsize], - "spa": [0, sys.maxsize], - "spd": [0, sys.maxsize], - "spe": [0, sys.maxsize], - } - - @staticmethod - def from_pokemon(mon: Pokemon): - if mon is None: - return None - - stats: Optional[Mapping[str, Union[List[int], int, None]]] = ( - ObservedPokemon.initial_stats() - ) - if mon.stats is not None: - stats = {k: v for (k, v) in mon.stats.items()} - - return ObservedPokemon( - species=mon.species, - level=mon.level, - ability=mon.ability, - boosts={k: v for (k, v) in mon.boosts.items()}, - current_hp_fraction=mon.current_hp_fraction, - effects={k: v for (k, v) in mon.effects.items()}, - is_dynamaxed=mon.is_dynamaxed, - is_terastallized=mon.is_terastallized, - item=mon.item, - gender=mon.gender, - moves={k: copy(v) for (k, v) in mon.moves.items()}, - tera_type=mon.tera_type, - shiny=mon.shiny, - stats=stats, - status=mon.status, - ) From 59564478273ceb968a18a6bb4a6f87a8931ef376 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 20:36:51 -0500 Subject: [PATCH 16/39] from past: cut observations out of abstract battle because it slows down deepcopy; revert to old save_replay system --- src/poke_env/environment/abstract_battle.py | 82 +-------------------- 1 file changed, 3 insertions(+), 79 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 7c579fe69..3fab873d3 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -7,8 +7,6 @@ from poke_env.data import GenData, to_id_str from poke_env.data.replay_template import REPLAY_TEMPLATE from poke_env.environment.field import Field -from poke_env.environment.observation import Observation -from poke_env.environment.observed_pokemon import ObservedPokemon from poke_env.environment.pokemon import Pokemon from poke_env.environment.side_condition import STACKABLE_CONDITIONS, SideCondition from poke_env.environment.weather import Weather @@ -77,7 +75,6 @@ class AbstractBattle(ABC): "_can_mega_evolve", "_can_tera", "_can_z_move", - "_current_observation", "_data", "_dynamax_turn", "_fields", @@ -90,7 +87,6 @@ class AbstractBattle(ABC): "_max_team_size", "_maybe_trapped", "_move_on_next_request", - "_observations", "_opponent_can_dynamax", "_opponent_can_mega_evolve", "_opponent_can_terrastallize", @@ -184,10 +180,6 @@ def __init__( self._team: Dict[str, Pokemon] = {} self._opponent_team: Dict[str, Pokemon] = {} - # Initialize Observations - self._observations: Dict[int, Observation] = {} - self._current_observation: Observation = Observation() - def get_pokemon( self, identifier: str, @@ -370,9 +362,6 @@ def field_start(self, field_str: str): self._fields[field] = self.turn def _finish_battle(self): - # Recording the battle state and save events as we finish up - self.observations[self.turn] = self._current_observation - if self._save_replays: if self._save_replays is True: folder = "replays" @@ -390,7 +379,6 @@ def _finish_battle(self): encoding="utf-8", ) as f: formatted_replay = REPLAY_TEMPLATE - formatted_replay = formatted_replay.replace( "{BATTLE_TAG}", f"{self.battle_tag}" ) @@ -401,23 +389,17 @@ def _finish_battle(self): "{OPPONENT_USERNAME}", f"{self._opponent_username}" ) replay_log = f">{self.battle_tag}" + "\n".join( - [ - "|".join(split_message) - for turn in sorted(self._observations.keys()) - for split_message in self._observations[turn].events - ] + ["|".join(split_message) for split_message in self._replay_data] ) formatted_replay = formatted_replay.replace("{REPLAY_LOG}", replay_log) - f.write(formatted_replay) self._finished = True def parse_message(self, split_message: List[str]): - self._current_observation.events.append(split_message) + if self._save_replays: + self._replay_data.append(split_message) - # We copy because we directly modify split_message in poke-env; this is to - # preserve further usage of this event upstream event = split_message[:] if event[1] in self.MESSAGES_TO_IGNORE: @@ -555,45 +537,7 @@ def parse_message(self, split_message: List[str]): pokemon, _ = event[2:4] self.get_pokemon(pokemon).cant_move() elif event[1] == "turn": - # Saving the beginning-of-turn battle state and events as we go into the turn - self.observations[self.turn] = self._current_observation - self.end_turn(int(event[2])) - - opp_active_mon, active_mon = None, None - if isinstance(self.opponent_active_pokemon, Pokemon): - opp_active_mon = ObservedPokemon.from_pokemon( - self.opponent_active_pokemon - ) - active_mon = ObservedPokemon.from_pokemon(self.active_pokemon) - else: - opp_active_mon = [ - ObservedPokemon.from_pokemon(mon) - for mon in self.opponent_active_pokemon - ] - active_mon = [ - ObservedPokemon.from_pokemon(mon) for mon in self.active_pokemon - ] - - # Create new Observation and record battle state going into the next turn - self._current_observation = Observation( - side_conditions={k: v for (k, v) in self.side_conditions.items()}, - opponent_side_conditions={ - k: v for (k, v) in self.opponent_side_conditions.items() - }, - weather={k: v for (k, v) in self.weather.items()}, - fields={k: v for (k, v) in self.fields.items()}, - active_pokemon=active_mon, - team={ - ident: ObservedPokemon.from_pokemon(mon) - for (ident, mon) in self.team.items() - }, - opponent_active_pokemon=opp_active_mon, - opponent_team={ - ident: ObservedPokemon.from_pokemon(mon) - for (ident, mon) in self.opponent_team.items() - }, - ) elif event[1] == "-heal": pokemon, hp_status = event[2:4] self.get_pokemon(pokemon).heal(hp_status) @@ -1050,16 +994,6 @@ def can_z_move(self) -> Any: def can_tera(self) -> Any: pass - @property - def current_observation(self) -> Observation: - """ - :return: The current observation of the current turn in the Battle. - Most useful for when a force_switch triggers in the middle of a - turn, and our player has to return an action. - :rtype: Observation - """ - return self._current_observation - @property def dynamax_turns_left(self) -> Optional[int]: """ @@ -1144,16 +1078,6 @@ def max_team_size(self) -> Optional[int]: def maybe_trapped(self) -> Any: pass - @property - def observations(self) -> Dict[int, Observation]: - """ - :return: Observations of the battle on a turn, where the key is the turn number. - The Observation stores the battle state at the beginning of the turn, - and all the events that transpired on that turn. - :rtype: Dict[int, Observation] - """ - return self._observations - @property @abstractmethod def opponent_active_pokemon(self) -> Any: From 533b686acdd140baf6babcf037972737334d4638 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 20:37:30 -0500 Subject: [PATCH 17/39] black module docstrings --- src/poke_env/environment/field.py | 3 +-- src/poke_env/environment/move_category.py | 3 +-- src/poke_env/environment/weather.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/poke_env/environment/field.py b/src/poke_env/environment/field.py index 83b5af6b7..430b020ef 100644 --- a/src/poke_env/environment/field.py +++ b/src/poke_env/environment/field.py @@ -1,5 +1,4 @@ -"""This module defines the Field class, which represents a battle field. -""" +"""This module defines the Field class, which represents a battle field.""" from __future__ import annotations diff --git a/src/poke_env/environment/move_category.py b/src/poke_env/environment/move_category.py index fa5b86c9a..e71137755 100644 --- a/src/poke_env/environment/move_category.py +++ b/src/poke_env/environment/move_category.py @@ -1,5 +1,4 @@ -"""This module defines the MoveCategory class, which represents a move category. -""" +"""This module defines the MoveCategory class, which represents a move category.""" from enum import Enum, auto, unique diff --git a/src/poke_env/environment/weather.py b/src/poke_env/environment/weather.py index 28fcc2dd6..401070beb 100644 --- a/src/poke_env/environment/weather.py +++ b/src/poke_env/environment/weather.py @@ -1,5 +1,4 @@ -"""This module defines the Weather class, which represents a in-battle weather. -""" +"""This module defines the Weather class, which represents a in-battle weather.""" import logging from enum import Enum, auto, unique From 0405fbb569bd559ee4def636ef931cfe863be09c Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 20:38:47 -0500 Subject: [PATCH 18/39] Battle deepcopy for (last_state, state) is now fast enough to justify keeping, and makes reward functions much easier to write --- src/poke_env/player/openai_api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/poke_env/player/openai_api.py b/src/poke_env/player/openai_api.py index 153663523..faeafd093 100644 --- a/src/poke_env/player/openai_api.py +++ b/src/poke_env/player/openai_api.py @@ -350,6 +350,18 @@ def step( return obs, 0.0, False, False, info if self.current_battle.finished: raise RuntimeError("Battle is already finished, call reset") + # This deepcopy was removed (412254) because it was a major slowdown (#451), + # and this led to a series of changes that made reward functions much harder to write. + # With no deepcopy, the last_battle was the same as the current_battle, so the + # r(last_battle, current_battle) broke (#662). ---> the reward function was + # changed to r(current_battle)(#671). ---> there was no way to write rews as + # the diff between two turns (e.g., net change in health). ---> a rew buffer + # was added to track the scalar reward from previous turns. ---> Now the rew + # funcs have hidden state, which is bad, and it's much easier to write rews with + # some terms that are net diffs than to write ones where taking the diff of the + # output value does what you want. + # + # --> roll back all of that, and make the deepcopy fast. battle = copy.deepcopy(self.current_battle) battle.logger = None self.last_battle = battle From 59badd67876df56e490defdfbc19258bef6fb04a Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 22:13:42 -0500 Subject: [PATCH 19/39] patch effect messages in from_data too --- src/poke_env/data/gen_data.py | 6 ++++-- src/poke_env/environment/effect.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/poke_env/data/gen_data.py b/src/poke_env/data/gen_data.py index c43833f82..047996805 100644 --- a/src/poke_env/data/gen_data.py +++ b/src/poke_env/data/gen_data.py @@ -73,8 +73,10 @@ def load_pokedex(self, gen: int) -> Dict[str, Any]: for name, value in dex.items(): if gen <= 2 and "abilities" in value: - # remove abilities from gen 1-2 - # JAKE: check if this is still the way to handle this + # remove abilities from gen 1-2. Gens before abilities + # existed will often list an "ability" called "No Ability". + # Because it is the only option, `Pokemon` will assume it + # is active at the start of the battle. value["abilities"] = {"0": "No Ability"} if "baseSpecies" in value: value["species"] = value["baseSpecies"] diff --git a/src/poke_env/environment/effect.py b/src/poke_env/environment/effect.py index 59d2377f4..17b79de75 100644 --- a/src/poke_env/environment/effect.py +++ b/src/poke_env/environment/effect.py @@ -245,6 +245,14 @@ class Effect(Enum): def __str__(self) -> str: return f"{self.name} (effect) object" + @staticmethod + def _manual_message_corrections(message: str) -> str: + if message == "FALLENUNDEFINED": + return "FALLEN" + elif message == "LIGHTSCREEN": + return "LIGHT_SCREEN" + return message + @staticmethod def from_showdown_message(message: str) -> Effect: """Returns the Effect object corresponding to the message. @@ -260,11 +268,7 @@ def from_showdown_message(message: str) -> Effect: message = message.replace(" ", "_") message = message.replace("-", "_") message = message.upper() - - if message == "FALLENUNDEFINED": - message = "FALLEN" - if message == "LIGHTSCREEN": - message = "LIGHT_SCREEN" + message = Effect._manual_message_corrections(message) try: return Effect[message] @@ -291,6 +295,8 @@ def from_data(message: str) -> Effect: message = message.replace(" ", "") message = message.replace("-", "") message = message.upper() + message = Effect._manual_message_corrections(message) + try: return _FROM_DATA[message] except KeyError: From d5e65ac1e249ef63da167268d9490bfe6495dec3 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 22:14:20 -0500 Subject: [PATCH 20/39] from metamon: 'recharge' move entry needs priority key --- src/poke_env/environment/move.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/poke_env/environment/move.py b/src/poke_env/environment/move.py index 69f2dea62..2d5dc549e 100644 --- a/src/poke_env/environment/move.py +++ b/src/poke_env/environment/move.py @@ -71,6 +71,7 @@ class Move: PokemonType.ROCK: MoveCategory.PHYSICAL, PokemonType.STEEL: MoveCategory.PHYSICAL, PokemonType.WATER: MoveCategory.SPECIAL, + PokemonType.THREE_QUESTION_MARKS: MoveCategory.PHYSICAL, } __slots__ = ( @@ -302,7 +303,14 @@ def entry(self) -> Dict[str, Any]: elif self._id.startswith("z") and self._id[1:] in self._moves_dict: return self._moves_dict[self._id[1:]] elif self._id == "recharge": - return {"pp": 1, "type": "normal", "category": "Special", "accuracy": 1} + return { + "pp": 1, + "type": "normal", + "category": "Special", + "accuracy": 1, + "priority": 0, + "target": "self", + } else: raise ValueError("Unknown move: %s" % self._id) From 77b91ad8cdd43151fb7729a56263f05895e19355 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Tue, 3 Jun 2025 22:15:22 -0500 Subject: [PATCH 21/39] handle common gen1 partial trapping unhandled move message --- src/poke_env/environment/abstract_battle.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 3fab873d3..18e8d07d1 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -430,11 +430,18 @@ def parse_message(self, split_message: List[str]): if event[-1].replace(" ", "") in { "[from]lockedmove", - "[from]Pursuit", "[zeffect]", + "[from]Pursuit", }: event = event[:-1] + if event[-1].startswith("[from]"): + # early gen partial trapping spam, for example: + # '', 'move', 'p1a: Tangela', 'Bind', 'p2a: Snorlax', '[from]Bind + maybe_from_move = event[-1][6:] + if maybe_from_move == event[3]: + event = event[:-1] + if event[-1].startswith("[anim]"): event = event[:-1] @@ -737,7 +744,7 @@ def parse_message(self, split_message: List[str]): target_mon = self.get_pokemon(target) if "[from]" in stats: if "guardswap" in stats: - # JAKE: need to use metamon to check if this ever triggers + # JAKE: need to use metamon to check if this still ever triggers all_stats = ["def", "spd"] else: all_stats = [ From af550d38285143fc3f37ec68cc777dd8f66eb3ae Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Thu, 5 Jun 2025 15:01:31 -0500 Subject: [PATCH 22/39] scale back scope of changes to this fork: manual merge effect and side_condition _FROM_DATA removal (#2) --- src/poke_env/environment/effect.py | 266 ++------------------- src/poke_env/environment/side_condition.py | 72 ++---- 2 files changed, 33 insertions(+), 305 deletions(-) diff --git a/src/poke_env/environment/effect.py b/src/poke_env/environment/effect.py index 17b79de75..d9809f251 100644 --- a/src/poke_env/environment/effect.py +++ b/src/poke_env/environment/effect.py @@ -273,14 +273,20 @@ def from_showdown_message(message: str) -> Effect: try: return Effect[message] except KeyError: - logging.getLogger("poke-env").warning( - "Unexpected effect '%s' received. Effect.UNKNOWN will be used instead. " - "If this is unexpected, please open an issue at " - "https://github.com/hsahovic/poke-env/issues/ along with this error " - "message and a description of your program.", - message, - ) - return Effect.UNKNOWN + # catch inconsistent use of whitespace both within Showdown protocol + # and between sim protocol and static data + for effect in Effect: + if effect.name.replace("_", "") == message: + return effect + # if we get here, we didn't find a match + logging.getLogger("poke-env").warning( + "Unexpected effect '%s' received. Effect.UNKNOWN will be used instead. " + "If this is unexpected, please open an issue at " + "https://github.com/hsahovic/poke-env/issues/ along with this error " + "message and a description of your program.", + message, + ) + return Effect.UNKNOWN @staticmethod def from_data(message: str) -> Effect: @@ -291,23 +297,9 @@ def from_data(message: str) -> Effect: :return: The corresponding Effect object. :rtype: Effect """ - message = message.replace("_", "") - message = message.replace(" ", "") - message = message.replace("-", "") - message = message.upper() - message = Effect._manual_message_corrections(message) - - try: - return _FROM_DATA[message] - except KeyError: - logging.getLogger("poke-env").warning( - "Unexpected effect '%s' received. Effect.UNKNOWN will be used instead. " - "If this is unexpected, please open an issue at " - "https://github.com/hsahovic/poke-env/issues/ along with this error " - "message and a description of your program.", - message, - ) - return Effect.UNKNOWN + # JAKE: this should no longer be necessary, but leave the door open for + # one-off changes specific to how the effects are stored in static data + return Effect.from_showdown_message(message) @property def breaks_protect(self): @@ -785,227 +777,3 @@ def is_from_move(self) -> bool: } _ACTION_COUNTER_EFFECTS: Set[Effect] = {Effect.RAGE, Effect.STOCKPILE} - -_FROM_DATA: Dict[str, Effect] = { - "UNKNOWN": Effect.UNKNOWN, - "AFTERYOU": Effect.AFTER_YOU, - "AFTERMATH": Effect.AFTERMATH, - "AQUARING": Effect.AQUA_RING, - "AROMATHERAPY": Effect.AROMATHERAPY, - "AROMAVEIL": Effect.AROMA_VEIL, - "ATTRACT": Effect.ATTRACT, - "AUTOTOMIZE": Effect.AUTOTOMIZE, - "BADDREAMS": Effect.BAD_DREAMS, - "BANEFULBUNKER": Effect.BANEFUL_BUNKER, - "BATTLEBOND": Effect.BATTLE_BOND, - "BIDE": Effect.BIDE, - "BIND": Effect.BIND, - "BURNINGBULWARK": Effect.BURNING_BULWARK, - "BURNUP": Effect.BURN_UP, - "CELEBRATE": Effect.CELEBRATE, - "CHARGE": Effect.CHARGE, - "CLAMP": Effect.CLAMP, - "COMMANDER": Effect.COMMANDER, - "CONFUSION": Effect.CONFUSION, - "COURTCHANGE": Effect.COURT_CHANGE, - "CRAFTYSHIELD": Effect.CRAFTY_SHIELD, - "CUDCHEW": Effect.CUD_CHEW, - "CURSE": Effect.CURSE, - "CUSTAPBERRY": Effect.CUSTAP_BERRY, - "DANCER": Effect.DANCER, - "DEFENSECURL": Effect.DEFENSE_CURL, - "DESTINYBOND": Effect.DESTINY_BOND, - "DISABLE": Effect.DISABLE, - "DISGUISE": Effect.DISGUISE, - "DOOMDESIRE": Effect.DOOM_DESIRE, - "DRAGONCHEER": Effect.DRAGON_CHEER, - "DYNAMAX": Effect.DYNAMAX, - "EERIESPELL": Effect.EERIE_SPELL, - "ELECTRICTERRAIN": Effect.ELECTRIC_TERRAIN, - "ELECTRIFY": Effect.ELECTRIFY, - "EMBARGO": Effect.EMBARGO, - "EMERGENCYEXIT": Effect.EMERGENCY_EXIT, - "ENCORE": Effect.ENCORE, - "ENDURE": Effect.ENDURE, - "FALLEN": Effect.FALLEN, - "FALLEN1": Effect.FALLEN1, - "FALLEN2": Effect.FALLEN2, - "FALLEN3": Effect.FALLEN3, - "FALLEN4": Effect.FALLEN4, - "FALLEN5": Effect.FALLEN5, - "FAIRYLOCK": Effect.FAIRY_LOCK, - "FEINT": Effect.FEINT, - "FICKLEBEAM": Effect.FICKLE_BEAM, - "FIRESPIN": Effect.FIRE_SPIN, - "FLASHFIRE": Effect.FLASH_FIRE, - "FLINCH": Effect.FLINCH, - "FLOWERVEIL": Effect.FLOWER_VEIL, - "FOCUSBAND": Effect.FOCUS_BAND, - "FOCUSENERGY": Effect.FOCUS_ENERGY, - "FOLLOWME": Effect.FOLLOW_ME, - "FORESIGHT": Effect.FORESIGHT, - "FOREWARN": Effect.FOREWARN, - "FUTURESIGHT": Effect.FUTURE_SIGHT, - "GASTROACID": Effect.GASTRO_ACID, - "GLAIVERUSH": Effect.GLAIVE_RUSH, - "GRAVITY": Effect.GRAVITY, - "GRUDGE": Effect.GRUDGE, - "GUARDSPLIT": Effect.GUARD_SPLIT, - "GULPMISSILE": Effect.GULP_MISSILE, - "GMAXCENTIFERNO": Effect.G_MAX_CENTIFERNO, - "GMAXCHISTRIKE": Effect.G_MAX_CHI_STRIKE, - "GMAXONEBLOW": Effect.G_MAX_ONE_BLOW, - "GMAXRAPIDFLOW": Effect.G_MAX_RAPID_FLOW, - "GMAXSANDBLAST": Effect.G_MAX_SANDBLAST, - "HADRONENGINE": Effect.HADRON_ENGINE, - "HEALBELL": Effect.HEAL_BELL, - "HEALBLOCK": Effect.HEAL_BLOCK, - "HEALER": Effect.HEALER, - "HELPINGHAND": Effect.HELPING_HAND, - "HYDRATION": Effect.HYDRATION, - "HYPERSPACEFURY": Effect.HYPERSPACE_FURY, - "HYPERSPACEHOLE": Effect.HYPERSPACE_HOLE, - "ICEFACE": Effect.ICE_FACE, - "ILLUSION": Effect.ILLUSION, - "IMMUNITY": Effect.IMMUNITY, - "IMPRISON": Effect.IMPRISON, - "INFESTATION": Effect.INFESTATION, - "INGRAIN": Effect.INGRAIN, - "INNARDSOUT": Effect.INNARDS_OUT, - "INSTRUCT": Effect.INSTRUCT, - "INSOMNIA": Effect.INSOMNIA, - "IRONBARBS": Effect.IRON_BARBS, - "KINGSSHIELD": Effect.KINGS_SHIELD, - "LASERFOCUS": Effect.LASER_FOCUS, - "LEECHSEED": Effect.LEECH_SEED, - "LEPPABERRY": Effect.LEPPA_BERRY, - "LIGHTNINGROD": Effect.LIGHTNING_ROD, - "LIMBER": Effect.LIMBER, - "LIQUIDOOZE": Effect.LIQUID_OOZE, - "LOCKEDMOVE": Effect.LOCKED_MOVE, - "LOCKON": Effect.LOCK_ON, - "MAGICCOAT": Effect.MAGIC_COAT, - "MAGMASTORM": Effect.MAGMA_STORM, - "MAGNETRISE": Effect.MAGNET_RISE, - "MAGNITUDE": Effect.MAGNITUDE, - "MATBLOCK": Effect.MAT_BLOCK, - "MAXGUARD": Effect.MAX_GUARD, - "MIMIC": Effect.MIMIC, - "MIMICRY": Effect.MIMICRY, - "MINDREADER": Effect.MIND_READER, - "MINIMIZE": Effect.MINIMIZE, - "MIRACLEEYE": Effect.MIRACLE_EYE, - "MIST": Effect.MIST, - "MISTYTERRAIN": Effect.MISTY_TERRAIN, - "MUMMY": Effect.MUMMY, - "MUSTRECHARGE": Effect.MUST_RECHARGE, - "NEUTRALIZINGGAS": Effect.NEUTRALIZING_GAS, - "NIGHTMARE": Effect.NIGHTMARE, - "NORETREAT": Effect.NO_RETREAT, - "OBLIVIOUS": Effect.OBLIVIOUS, - "OBSTRUCT": Effect.OBSTRUCT, - "OCTOLOCK": Effect.OCTOLOCK, - "ORICHALCUMPULSE": Effect.ORICHALCUM_PULSE, - "OWNTEMPO": Effect.OWN_TEMPO, - "PARTIALLYTRAPPED": Effect.PARTIALLY_TRAPPED, - "PASTELVEIL": Effect.PASTEL_VEIL, - "PERISH0": Effect.PERISH0, - "PERISH1": Effect.PERISH1, - "PERISH2": Effect.PERISH2, - "PERISH3": Effect.PERISH3, - "PHANTOMFORCE": Effect.PHANTOM_FORCE, - "POLTERGEIST": Effect.POLTERGEIST, - "POWDER": Effect.POWDER, - "POWERCONSTRUCT": Effect.POWER_CONSTRUCT, - "POWERSHIFT": Effect.POWER_SHIFT, - "POWERSPLIT": Effect.POWER_SPLIT, - "POWERTRICK": Effect.POWER_TRICK, - "PROTECT": Effect.PROTECT, - "PROTECTIVEPADS": Effect.PROTECTIVE_PADS, - "PROTOSYNTHESIS": Effect.PROTOSYNTHESIS, - "PROTOSYNTHESISATK": Effect.PROTOSYNTHESISATK, - "PROTOSYNTHESISDEF": Effect.PROTOSYNTHESISDEF, - "PROTOSYNTHESISSPA": Effect.PROTOSYNTHESISSPA, - "PROTOSYNTHESISSPD": Effect.PROTOSYNTHESISSPD, - "PROTOSYNTHESISSPE": Effect.PROTOSYNTHESISSPE, - "PSYCHICTERRAIN": Effect.PSYCHIC_TERRAIN, - "PURSUIT": Effect.PURSUIT, - "QUARKDRIVE": Effect.QUARK_DRIVE, - "QUARKDRIVEATK": Effect.QUARKDRIVEATK, - "QUARKDRIVEDEF": Effect.QUARKDRIVEDEF, - "QUARKDRIVESPA": Effect.QUARKDRIVESPA, - "QUARKDRIVESPD": Effect.QUARKDRIVESPD, - "QUARKDRIVESPE": Effect.QUARKDRIVESPE, - "QUASH": Effect.QUASH, - "QUICKCLAW": Effect.QUICK_CLAW, - "QUICKDRAW": Effect.QUICK_DRAW, - "QUICKGUARD": Effect.QUICK_GUARD, - "RAGE": Effect.RAGE, - "RAGEPOWDER": Effect.RAGE_POWDER, - "REFLECT": Effect.REFLECT, - "RIPEN": Effect.RIPEN, - "ROOST": Effect.ROOST, - "ROUGHSKIN": Effect.ROUGH_SKIN, - "SAFEGUARD": Effect.SAFEGUARD, - "SAFETYGOGGLES": Effect.SAFETY_GOGGLES, - "SALTCURE": Effect.SALT_CURE, - "SANDTOMB": Effect.SAND_TOMB, - "SCREENCLEANER": Effect.SCREEN_CLEANER, - "SHADOWFORCE": Effect.SHADOW_FORCE, - "SHEDSKIN": Effect.SHED_SKIN, - "SILKTRAP": Effect.SILK_TRAP, - "SKETCH": Effect.SKETCH, - "SKILLSWAP": Effect.SKILL_SWAP, - "SKYDROP": Effect.SKY_DROP, - "SLOWSTART": Effect.SLOW_START, - "SMACKDOWN": Effect.SMACK_DOWN, - "SNAPTRAP": Effect.SNAP_TRAP, - "SNATCH": Effect.SNATCH, - "SPARKLINGARIA": Effect.SPARKLING_ARIA, - "SPEEDSWAP": Effect.SPEED_SWAP, - "SPIKYSHIELD": Effect.SPIKY_SHIELD, - "SPITE": Effect.SPITE, - "SPOTLIGHT": Effect.SPOTLIGHT, - "STICKY_HOLD": Effect.STICKY_HOLD, - "STICKY_WEB": Effect.STICKY_WEB, - "STOCKPILE": Effect.STOCKPILE, - "STOCKPILE1": Effect.STOCKPILE1, - "STOCKPILE2": Effect.STOCKPILE2, - "STOCKPILE3": Effect.STOCKPILE3, - "STORMDRAIN": Effect.STORM_DRAIN, - "STRUGGLE": Effect.STRUGGLE, - "SUBSTITUTE": Effect.SUBSTITUTE, - "SUCTIONCUPS": Effect.SUCTION_CUPS, - "SUPREMEOVERLORD": Effect.SUPREME_OVERLORD, - "SYRUPBOMB": Effect.SYRUP_BOMB, - "SWEETVEIL": Effect.SWEET_VEIL, - "SYMBIOSIS": Effect.SYMBIOSIS, - "SYNCHRONIZE": Effect.SYNCHRONIZE, - "TARSHOT": Effect.TAR_SHOT, - "TAUNT": Effect.TAUNT, - "TELEKINESIS": Effect.TELEKINESIS, - "TELEPATHY": Effect.TELEPATHY, - "TERASHELL": Effect.TERA_SHELL, - "TERASHIFT": Effect.TERA_SHIFT, - "TIDYUP": Effect.TIDY_UP, - "TOXICDEBRIS": Effect.TOXIC_DEBRIS, - "THERMALEXCHANGE": Effect.THERMAL_EXCHANGE, - "THROATCHOP": Effect.THROAT_CHOP, - "THUNDERCAGE": Effect.THUNDER_CAGE, - "TORMENT": Effect.TORMENT, - "TRAPPED": Effect.TRAPPED, - "TRICK": Effect.TRICK, - "TYPEADD": Effect.TYPEADD, - "TYPECHANGE": Effect.TYPECHANGE, - "UPROAR": Effect.UPROAR, - "VITALSPIRIT": Effect.VITAL_SPIRIT, - "WANDERINGSPIRIT": Effect.WANDERING_SPIRIT, - "WATERBUBBLE": Effect.WATER_BUBBLE, - "WATERVEIL": Effect.WATER_VEIL, - "WHIRLPOOL": Effect.WHIRLPOOL, - "WIDEGUARD": Effect.WIDE_GUARD, - "WIMPOUT": Effect.WIMP_OUT, - "WRAP": Effect.WRAP, - "YAWN": Effect.YAWN, - "ZERO_TO_HERO": Effect.ZERO_TO_HERO, -} diff --git a/src/poke_env/environment/side_condition.py b/src/poke_env/environment/side_condition.py index 94fcf1c89..51ac4075f 100644 --- a/src/poke_env/environment/side_condition.py +++ b/src/poke_env/environment/side_condition.py @@ -51,23 +51,23 @@ def from_showdown_message(message: str): message = message.replace("move: ", "") message = message.replace(" ", "_") message = message.replace("-", "_") - condition_code = message.upper() + message = message.upper() try: - return SideCondition[condition_code] + return SideCondition[message] except KeyError: - # hack attempt to catch conditions w/ inconsistent spacing (mainly in old replays) - # JAKE: recheck this on v0.8.3 + # catch inconsistent use of whitespace both within Showdown protocol + # and between sim protocol and static data for known_condition in SideCondition: - if known_condition.name.replace("_", "") == condition_code: + if known_condition.name.replace("_", "") == message: return known_condition - logging.getLogger("poke-env").warning( - "Unexpected side condition '%s' received. SideCondition.UNKNOWN will be" - " used instead. If this is unexpected, please open an issue at " - "https://github.com/hsahovic/poke-env/issues/ along with this error " - "message and a description of your program.", - message, - ) - return SideCondition.UNKNOWN + logging.getLogger("poke-env").warning( + "Unexpected side condition '%s' received. SideCondition.UNKNOWN will be" + " used instead. If this is unexpected, please open an issue at " + "https://github.com/hsahovic/poke-env/issues/ along with this error " + "message and a description of your program.", + message, + ) + return SideCondition.UNKNOWN @staticmethod def from_data(message: str): @@ -78,50 +78,10 @@ def from_data(message: str): :return: The corresponding SideCondition object. :rtype: SideCondition """ - message = message.replace("_", "") - message = message.replace(" ", "") - message = message.replace("-", "") - message = message.upper() - - try: - return _FROM_DATA[message] - except KeyError: - logging.getLogger("poke-env").warning( - "Unexpected SideCondition '%s' received. SideCondition.UNKNOWN will be used " - "instead. If this is unexpected, please open an issue at " - "https://github.com/hsahovic/poke-env/issues/ along with this error " - "message and a description of your program.", - message, - ) - return SideCondition.UNKNOWN + # JAKE: this should no longer be necessary, but leave the door open for + # one-off changes specific to how the side conditions are stored in static data + return SideCondition.from_showdown_message(message) # SideCondition -> Max useful stack level STACKABLE_CONDITIONS = {SideCondition.SPIKES: 3, SideCondition.TOXIC_SPIKES: 2} - -_FROM_DATA: Dict[str, SideCondition] = { - "UNKNOWN": SideCondition.UNKNOWN, - "AURORAVEIL": SideCondition.AURORA_VEIL, - "CRAFTYSHIELD": SideCondition.CRAFTY_SHIELD, - "FIREPLEDGE": SideCondition.FIRE_PLEDGE, - "GMAXCANNONADE": SideCondition.G_MAX_CANNONADE, - "GMAXSTEELSURGE": SideCondition.G_MAX_STEELSURGE, - "GMAXVINELASH": SideCondition.G_MAX_VINE_LASH, - "GMAXVOLCALITH": SideCondition.G_MAX_VOLCALITH, - "GMAXWILDFIRE": SideCondition.G_MAX_WILDFIRE, - "GRASSPLEDGE": SideCondition.GRASS_PLEDGE, - "LIGHTSCREEN": SideCondition.LIGHT_SCREEN, - "LUCKYCHANT": SideCondition.LUCKY_CHANT, - "MATBLOCK": SideCondition.MATBLOCK, - "MIST": SideCondition.MIST, - "QUICKGUARD": SideCondition.QUICK_GUARD, - "REFLECT": SideCondition.REFLECT, - "SAFEGUARD": SideCondition.SAFEGUARD, - "SPIKES": SideCondition.SPIKES, - "STEALTHROCK": SideCondition.STEALTH_ROCK, - "STICKYWEB": SideCondition.STICKY_WEB, - "TAILWIND": SideCondition.TAILWIND, - "TOXICSPIKES": SideCondition.TOXIC_SPIKES, - "WATERPLEDGE": SideCondition.WATER_PLEDGE, - "WIDEGUARD": SideCondition.WIDE_GUARD, -} From 1b540a95ff92dfef83889957e621e2c6aeba8c6f Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Thu, 5 Jun 2025 20:49:41 -0500 Subject: [PATCH 23/39] draft: switch to a custom player in openaigym env --- src/poke_env/player/openai_api.py | 82 +++++++++++++++++-------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/src/poke_env/player/openai_api.py b/src/poke_env/player/openai_api.py index faeafd093..0d726fddb 100644 --- a/src/poke_env/player/openai_api.py +++ b/src/poke_env/player/openai_api.py @@ -10,7 +10,7 @@ import time from abc import ABC, abstractmethod from logging import Logger -from typing import Any, Awaitable, Callable, Dict, Generic, List, Optional, Tuple, Union +from typing import Any, Awaitable, Callable, Dict, Generic, List, Optional, Tuple, Union, Type from gymnasium.core import ActType, Env, ObsType from gymnasium.spaces import Discrete, Space @@ -56,42 +56,50 @@ async def async_join(self): await self.queue.join() -class _AsyncPlayer(Generic[ObsType, ActType], Player): - actions: _AsyncQueue - observations: _AsyncQueue +def _create_async_player_class(parent_class: Type[Player]) -> Type[Player]: - def __init__( - self, - user_funcs: OpenAIGymEnv[ObsType, ActType], - username: str, - **kwargs: Any, - ): - self.__class__.__name__ = username - super().__init__(**kwargs) - self.__class__.__name__ = "_AsyncPlayer" - self.observations = _AsyncQueue(create_in_poke_loop(asyncio.Queue, 1)) - self.actions = _AsyncQueue(create_in_poke_loop(asyncio.Queue, 1)) - self.current_battle: Optional[AbstractBattle] = None - self._user_funcs = user_funcs + class _AsyncPlayer(Generic[ObsType, ActType], parent_class): + actions: _AsyncQueue + observations: _AsyncQueue - def choose_move(self, battle: AbstractBattle) -> Awaitable[BattleOrder]: - return self._env_move(battle) - - async def _env_move(self, battle: AbstractBattle) -> BattleOrder: - if not self.current_battle or self.current_battle.finished: - self.current_battle = battle - if not self.current_battle == battle: - raise RuntimeError("Using different battles for queues") - battle_to_send = self._user_funcs.embed_battle(battle) - await self.observations.async_put(battle_to_send) - action = await self.actions.async_get() - if action == -1: - return ForfeitBattleOrder() - return self._user_funcs.action_to_move(action, battle) - - def _battle_finished_callback(self, battle: AbstractBattle): - to_put = self._user_funcs.embed_battle(battle) - asyncio.run_coroutine_threadsafe(self.observations.async_put(to_put), POKE_LOOP) + def __init__( + self, + user_funcs: OpenAIGymEnv[ObsType, ActType], + username: str, + **kwargs: Any, + ): + self.__class__.__name__ = username + super().__init__(**kwargs) + self.__class__.__name__ = "_AsyncPlayer" + self.observations = _AsyncQueue(create_in_poke_loop(asyncio.Queue, 1)) + self.actions = _AsyncQueue(create_in_poke_loop(asyncio.Queue, 1)) + self.current_battle: Optional[AbstractBattle] = None + self._user_funcs = user_funcs + + def choose_move(self, battle: AbstractBattle) -> Awaitable[BattleOrder]: + return self._env_move(battle) + + async def _env_move(self, battle: AbstractBattle) -> BattleOrder: + if not self.current_battle or self.current_battle.finished: + self.current_battle = battle + if not self.current_battle == battle: + raise RuntimeError("Using different battles for queues") + battle_to_send = self._user_funcs.embed_battle(battle) + await self.observations.async_put(battle_to_send) + action = await self.actions.async_get() + if action == -1: + return ForfeitBattleOrder() + return self._user_funcs.action_to_move(action, battle) + + def _battle_finished_callback(self, battle: AbstractBattle): + to_put = self._user_funcs.embed_battle(battle) + asyncio.run_coroutine_threadsafe(self.observations.async_put(to_put), POKE_LOOP) + + return _AsyncPlayer + + +# Create the default _AsyncPlayer class with Player as parent +_AsyncPlayer = _create_async_player_class(Player) class OpenAIGymEnv( @@ -109,6 +117,7 @@ class OpenAIGymEnv( def __init__( self, + player_class: Type[Player] = Player, account_configuration: Optional[AccountConfiguration] = None, *, avatar: Optional[int] = None, @@ -170,7 +179,8 @@ def __init__( leave it inactive. :type start_challenging: bool """ - self.agent = _AsyncPlayer( + player_class = _create_async_player_class(player_class) + self.agent = player_class( self, username=self.__class__.__name__, # type: ignore account_configuration=account_configuration, From 842830fd4b601dd409df1a6a7437b79c11d72aa0 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Thu, 5 Jun 2025 23:04:55 -0500 Subject: [PATCH 24/39] from metamon: leave AbstractBattle set to version from paper's fork. Fix parsing in new version --- src/poke_env/environment/abstract_battle.py | 44 +++++++++------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 18e8d07d1..649701c6f 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -413,6 +413,9 @@ def parse_message(self, split_message: List[str]): self._check_damage_message_for_item(event) self._check_damage_message_for_ability(event) elif event[1] == "move": + # JAKE: this is inaccurate for early gens, but for some reason I left it like this + # during all the metamon evals. Now that we'll be doing a new `Battle`, leave as it + # was for backwards compatibility. failed = False override_move = None reveal_other_move = False @@ -428,20 +431,9 @@ def parse_message(self, split_message: List[str]): if event[-1].startswith("[spread]"): event = event[:-1] - if event[-1].replace(" ", "") in { - "[from]lockedmove", - "[zeffect]", - "[from]Pursuit", - }: + if event[-1] in {"[from]lockedmove", "[from]Pursuit", "[zeffect]"}: event = event[:-1] - if event[-1].startswith("[from]"): - # early gen partial trapping spam, for example: - # '', 'move', 'p1a: Tangela', 'Bind', 'p2a: Snorlax', '[from]Bind - maybe_from_move = event[-1][6:] - if maybe_from_move == event[3]: - event = event[:-1] - if event[-1].startswith("[anim]"): event = event[:-1] @@ -453,14 +445,12 @@ def parse_message(self, split_message: List[str]): reveal_other_move = True elif override_move in {"Copycat", "Metronome", "Nature Power"}: pass - elif override_move in {"Grass Pledge", "Water Pledge", "Fire Pledge"}: - override_move = None elif self.logger is not None: self.logger.warning( "Unmanaged [from]move message received - move %s in cleaned up " "message %s in battle %s turn %d", override_move, - event, + split_message, self.battle_tag, self.turn, ) @@ -470,7 +460,7 @@ def parse_message(self, split_message: List[str]): if event[-1].startswith("[from]ability: "): revealed_ability = event.pop()[15:] - pokemon = event[2] + pokemon = split_message[2] self.get_pokemon(pokemon).ability = revealed_ability if revealed_ability == "Magic Bounce": @@ -482,7 +472,7 @@ def parse_message(self, split_message: List[str]): "Unmanaged [from]ability: message received - ability %s in " "cleaned up message %s in battle %s turn %d", revealed_ability, - event, + split_message, self.battle_tag, self.turn, ) @@ -519,14 +509,17 @@ def parse_message(self, split_message: List[str]): ) else: pokemon, move, presumed_target = event[2:5] - if self.logger is not None: - self.logger.warning( - "Unmanaged move message format received - cleaned up message %s in " - "battle %s turn %d", - event, - self.battle_tag, - self.turn, - ) + if len(event) == 6 and "[from]" in event[-1]: + pass + else: + if self.logger is not None: + self.logger.warning( + "Unmanaged move message format received - cleaned up message %s in " + "battle %s turn %d", + event, + self.battle_tag, + self.turn, + ) # Check if a silent-effect move has occurred (Minimize) and add the effect if move.upper().strip() == "MINIMIZE": @@ -540,6 +533,7 @@ def parse_message(self, split_message: List[str]): self.get_pokemon(pokemon).moved(override_move, failed=failed, use=False) if override_move is None or reveal_other_move: self.get_pokemon(pokemon).moved(move, failed=failed, use=False) + elif event[1] == "cant": pokemon, _ = event[2:4] self.get_pokemon(pokemon).cant_move() From 0975d17b8c5e82dce432b2456ae0460b6bb5b173 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Sun, 8 Jun 2025 22:48:26 -0500 Subject: [PATCH 25/39] add empty flags to recharge entry --- src/poke_env/environment/move.py | 1 + src/poke_env/environment/pokemon.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/poke_env/environment/move.py b/src/poke_env/environment/move.py index 2d5dc549e..79bb5341c 100644 --- a/src/poke_env/environment/move.py +++ b/src/poke_env/environment/move.py @@ -309,6 +309,7 @@ def entry(self) -> Dict[str, Any]: "category": "Special", "accuracy": 1, "priority": 0, + "flags" : {}, "target": "self", } else: diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index 61885a9b1..d0e57f9c7 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -646,7 +646,7 @@ def available_moves_from_request(self, request: Dict[str, Any]) -> List[Move]: [v for m, v in self.moves.items() if m.startswith("hiddenpower")][0] ) else: - assert { + plausible_reasons_to_discover = { "copycat", "metronome", "mefirst", @@ -654,10 +654,11 @@ def available_moves_from_request(self, request: Dict[str, Any]) -> List[Move]: "assist", "transform", "mimic", - }.intersection(self.moves), ( + } + assert plausible_reasons_to_discover.intersection(self.moves), ( f"Error with move {move}. Expected self.moves to contain copycat, " "metronome, mefirst, mirrormove, assist, transform or mimic. Got" - f" {self.moves}" + f" {self.moves.keys()}" ) moves.append(Move(move, gen=self._data.gen)) return moves From 4856ea48b690559feadee22d5331e8af7352cf94 Mon Sep 17 00:00:00 2001 From: jakegrigsby Date: Mon, 9 Jun 2025 10:57:55 -0500 Subject: [PATCH 26/39] from future: handle new Showdown protocol->request order --- src/poke_env/environment/abstract_battle.py | 15 --------------- src/poke_env/environment/battle.py | 3 --- src/poke_env/player/player.py | 20 +++++++++----------- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 649701c6f..d5101c902 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -86,7 +86,6 @@ class AbstractBattle(ABC): "_last_request", "_max_team_size", "_maybe_trapped", - "_move_on_next_request", "_opponent_can_dynamax", "_opponent_can_mega_evolve", "_opponent_can_terrastallize", @@ -151,7 +150,6 @@ def __init__( # Turn choice attributes self.in_team_preview: bool = False - self._move_on_next_request: bool = False self._wait: Optional[bool] = None # Battle state attributes @@ -1333,19 +1331,6 @@ def won(self) -> Optional[bool]: """ return self._won - @property - def move_on_next_request(self) -> bool: - """ - :return: Wheter the next received request should yield a move order directly. - This can happen when a switch is forced, or an error is encountered. - :rtype: bool - """ - return self._move_on_next_request - - @move_on_next_request.setter - def move_on_next_request(self, value: bool): - self._move_on_next_request = value - @property def reviving(self) -> bool: return self._reviving diff --git a/src/poke_env/environment/battle.py b/src/poke_env/environment/battle.py index 655baf728..9c22b2176 100644 --- a/src/poke_env/environment/battle.py +++ b/src/poke_env/environment/battle.py @@ -81,9 +81,6 @@ def parse_request(self, request: Dict[str, Any]) -> None: self._trapped = False self._force_switch = request.get("forceSwitch", [False])[0] - if self._force_switch: - self._move_on_next_request = True - self._last_request = request if request.get("teamPreview", False): diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index 9ec979a21..52e07d5c7 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -143,6 +143,8 @@ def __init__( ) self._battle_end_condition: Condition = create_in_poke_loop(Condition) self._challenge_queue: Queue[Any] = create_in_poke_loop(Queue) + self._waiting: Event = create_in_poke_loop(Event) + self._trying_again: Event = create_in_poke_loop(Event) self._team: Optional[Teambuilder] = None if isinstance(team, Teambuilder): @@ -275,9 +277,10 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): if split_message[2]: request = orjson.loads(split_message[2]) battle.parse_request(request) - if battle.move_on_next_request: + if battle._wait: + self._waiting.set() + else: await self._handle_battle_request(battle) - battle.move_on_next_request = False elif split_message[1] == "win" or split_message[1] == "tie": if split_message[1] == "win": battle.won_by(split_message[2]) @@ -298,15 +301,14 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): "[Invalid choice] Sorry, too late to make a different move" ): if battle.trapped: - await self._handle_battle_request(battle) + self._trying_again.set() elif split_message[2].startswith( "[Unavailable choice] Can't switch: The active Pokémon is " "trapped" ) or split_message[2].startswith( "[Invalid choice] Can't switch: The active Pokémon is trapped" ): - battle.trapped = True - await self._handle_battle_request(battle) + self._trying_again.set() elif split_message[2].startswith( "[Invalid choice] Can't switch: You can't switch to an active " "Pokémon" @@ -357,12 +359,6 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): await self._handle_battle_request(battle, maybe_default_order=True) else: self.logger.critical("Unexpected error message: %s", split_message) - elif split_message[1] == "turn": - battle.parse_message(split_message) - await self._handle_battle_request(battle) - elif split_message[1] == "teampreview": - battle.parse_message(split_message) - await self._handle_battle_request(battle, from_teampreview_request=True) elif split_message[1] == "bigerror": self.logger.warning("Received 'bigerror' message: %s", split_message) elif split_message[1] == "uhtml" and split_message[2] == "otsrequest": @@ -383,6 +379,8 @@ async def _handle_battle_request( return message = self.teampreview(battle) else: + if maybe_default_order: + self._trying_again.set() choice = self.choose_move(battle) if isinstance(choice, Awaitable): choice = await choice From 7d618d6bba9c8e7ce883fea1ed7069424de33ea9 Mon Sep 17 00:00:00 2001 From: jakegrigsby Date: Mon, 9 Jun 2025 16:48:59 -0500 Subject: [PATCH 27/39] remove move discovery assert that fails for valid reasons --- src/poke_env/environment/move.py | 2 +- src/poke_env/environment/pokemon.py | 19 +++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/poke_env/environment/move.py b/src/poke_env/environment/move.py index 79bb5341c..13d5a3439 100644 --- a/src/poke_env/environment/move.py +++ b/src/poke_env/environment/move.py @@ -309,7 +309,7 @@ def entry(self) -> Dict[str, Any]: "category": "Special", "accuracy": 1, "priority": 0, - "flags" : {}, + "flags": {}, "target": "self", } else: diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index d0e57f9c7..b740c07a8 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -646,20 +646,11 @@ def available_moves_from_request(self, request: Dict[str, Any]) -> List[Move]: [v for m, v in self.moves.items() if m.startswith("hiddenpower")][0] ) else: - plausible_reasons_to_discover = { - "copycat", - "metronome", - "mefirst", - "mirrormove", - "assist", - "transform", - "mimic", - } - assert plausible_reasons_to_discover.intersection(self.moves), ( - f"Error with move {move}. Expected self.moves to contain copycat, " - "metronome, mefirst, mirrormove, assist, transform or mimic. Got" - f" {self.moves.keys()}" - ) + # JAKE: almost always means stolen/imitated/dynamic movesets + # (transform, mimic, etc.). There used to be a sanity-check + # for those moves here, but it fails for known reasons, as + # the move discovery system doesn't have enough info to handle + # the edge cases. moves.append(Move(move, gen=self._data.gen)) return moves From 13350d2b29f6d13b43debeea1e35be7ff6cfba52 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Mon, 9 Jun 2025 17:58:49 -0500 Subject: [PATCH 28/39] Update README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 07ee14403..f7db5384f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ +>[!IMPORTANT] +> This is a fork of [poke-env](https://github.com/hsahovic/poke-env) that installs with [`metamon`](https://github.com/UT-Austin-RPL/metamon). It attempts to extend the lifespan of poke-env as it was during Metamon's development: +> 1. Maintains the original gymnasium interface that existed until v0.8.3. `OpenAIGymEnv` (+ ability to swap in custom Players). Rewards functions that take `last_battle` and `current_battle` as input (+ a speed bost). Removes "observation" system that slows fps and is already handled by Metamon. +> 2. Preserves minor early-generation battle details as they were when Metamon's original models were trained. +> 3. Tries to bring key fixes/improvements since v0.8.3 that are unrelated to gymnasium. +> +> Please see the main repo [here](https://github.com/hsahovic/poke-env) for any other use case. I only plan to update this to fix breaking changes to the Showdown sim/request message API. Any improvements to early-generation state tracking/sim protocol are now done in metamon. + + + + + # The pokemon showdown Python environment [![PyPI version fury.io](https://badge.fury.io/py/poke-env.svg)](https://pypi.python.org/pypi/poke-env/) From 9de24eeb304cfa6a85100b8dc1c4efe3558da715 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Mon, 9 Jun 2025 17:59:47 -0500 Subject: [PATCH 29/39] Update README.md --- README.md | 94 ------------------------------------------------------- 1 file changed, 94 deletions(-) diff --git a/README.md b/README.md index f7db5384f..b420a11e4 100644 --- a/README.md +++ b/README.md @@ -7,100 +7,6 @@ > Please see the main repo [here](https://github.com/hsahovic/poke-env) for any other use case. I only plan to update this to fix breaking changes to the Showdown sim/request message API. Any improvements to early-generation state tracking/sim protocol are now done in metamon. - - - -# The pokemon showdown Python environment - -[![PyPI version fury.io](https://badge.fury.io/py/poke-env.svg)](https://pypi.python.org/pypi/poke-env/) -[![PyPI pyversions](https://img.shields.io/pypi/pyversions/poke-env.svg)](https://pypi.python.org/pypi/poke-env/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Documentation Status](https://readthedocs.org/projects/poke-env/badge/?version=stable)](https://poke-env.readthedocs.io/en/stable/?badge=stable) -[![codecov](https://codecov.io/gh/hsahovic/poke-env/branch/master/graph/badge.svg)](https://codecov.io/gh/hsahovic/poke-env) - -A Python interface to create battling pokemon agents. `poke-env` offers an easy-to-use interface for creating rule-based or training Reinforcement Learning bots to battle on [pokemon showdown](https://pokemonshowdown.com/). - -![A simple agent in action](rl-gif.gif) - -## Getting started - -Agents are instance of python classes inheriting from `Player`. Here is what your first agent could look like: - -```python -class YourFirstAgent(Player): - def choose_move(self, battle): - for move in battle.available_moves: - if move.base_power > 90: - # A powerful move! Let's use it - return self.create_order(move) - - # No available move? Let's switch then! - for switch in battle.available_switches: - if switch.current_hp_fraction > battle.active_pokemon.current_hp_fraction: - # This other pokemon has more HP left... Let's switch it in? - return self.create_order(switch) - - # Not sure what to do? - return self.choose_random_move(battle) -``` - -To get started, take a look at [our documentation](https://poke-env.readthedocs.io/en/stable/)! - - -## Documentation and examples - -Documentation, detailed examples and starting code can be found [on readthedocs](https://poke-env.readthedocs.io/en/stable/). - - -## Installation - -This project requires python >= 3.9 and a [Pokemon Showdown](https://github.com/Zarel/Pokemon-Showdown) server. - -``` -pip install poke-env -``` - -You can use [smogon's server](https://play.pokemonshowdown.com/) to try out your agents against humans, but having a development server is strongly recommended. In particular, it is recommended to use the `--no-security` flag to run a local server with most rate limiting and throttling turned off. Please refer to [the docs](https://poke-env.readthedocs.io/en/stable/getting_started.html#configuring-a-showdown-server) for detailed setup instructions. - - -``` -git clone https://github.com/smogon/pokemon-showdown.git -cd pokemon-showdown -npm install -cp config/config-example.js config/config.js -node pokemon-showdown start --no-security -``` - -## Development version - -You can also clone the latest master version with: - -``` -git clone https://github.com/hsahovic/poke-env.git -``` - -Dependencies and development dependencies can then be installed with: - -``` -pip install -r requirements.txt -pip install -r requirements-dev.txt -``` - -## Acknowledgements - -This project is a follow-up of a group project from an artifical intelligence class at [Ecole Polytechnique](https://www.polytechnique.edu/). - -You can find the original repository [here](https://github.com/hsahovic/inf581-project). It is partially inspired by the [showdown-battle-bot project](https://github.com/Synedh/showdown-battle-bot). Of course, none of these would have been possible without [Pokemon Showdown](https://github.com/Zarel/Pokemon-Showdown). - -Team data comes from [Smogon forums' RMT section](https://www.smogon.com/). - -## Data - -Data files are adapted version of the `js` data files of [Pokemon Showdown](https://github.com/Zarel/Pokemon-Showdown). - -## License -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - ## Citing `poke-env` ```bibtex From f096f8aca2bde89ad7658d8f70f829f9b9d0c204 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Mon, 9 Jun 2025 23:21:39 -0500 Subject: [PATCH 30/39] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c4e6d563..09c65689e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "poke_env" -version = "0.8.3" +version = "0.8.3.1" description = "A python interface for training Reinforcement Learning bots to battle on pokemon showdown." readme = "README.md" requires-python = ">=3.9.0" From 629a3d42f2e315fcdae56b44153998606d349dd1 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Wed, 25 Jun 2025 00:53:08 -0500 Subject: [PATCH 31/39] add snowscape weather --- src/poke_env/environment/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/poke_env/environment/weather.py b/src/poke_env/environment/weather.py index 401070beb..b245612ec 100644 --- a/src/poke_env/environment/weather.py +++ b/src/poke_env/environment/weather.py @@ -16,6 +16,7 @@ class Weather(Enum): RAINDANCE = auto() SANDSTORM = auto() SNOW = auto() + SNOWSCAPE = auto() SUNNYDAY = auto() def __str__(self) -> str: From 793b74940539f44165564ead06773fd7182becc4 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Wed, 25 Jun 2025 01:29:35 -0500 Subject: [PATCH 32/39] from future: fix port of sim/request order to work with teampreview --- src/poke_env/player/player.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index 52e07d5c7..b6f1f7851 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -369,14 +369,11 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): async def _handle_battle_request( self, battle: AbstractBattle, - from_teampreview_request: bool = False, maybe_default_order: bool = False, ): if maybe_default_order and random.random() < self.DEFAULT_CHOICE_CHANCE: message = self.choose_default_move().message elif battle.teampreview: - if not from_teampreview_request: - return message = self.teampreview(battle) else: if maybe_default_order: From cc45463bcb9cf9a4fb1d1f6aee9343434bf14c00 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Wed, 25 Jun 2025 03:01:02 -0500 Subject: [PATCH 33/39] from future: defend against -prepare ... [premajor] --- src/poke_env/environment/abstract_battle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index d5101c902..bd2261f9b 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -705,8 +705,8 @@ def parse_message(self, split_message: List[str]): elif event[1] == "-prepare": try: attacker, move, defender = event[2:5] - defender_mon = self.get_pokemon(defender) - if to_id_str(move) == "skydrop": + defender_mon = self.get_pokemon(defender) if defender != "[premajor]" else None + if defender_mon is not None and to_id_str(move) == "skydrop": defender_mon.start_effect("Sky Drop") except ValueError: attacker, move = event[2:4] From deeb44f3e22bf87e6a1f1ccaa2b254113589e687 Mon Sep 17 00:00:00 2001 From: jakegrigsby Date: Sat, 5 Jul 2025 15:07:29 -0500 Subject: [PATCH 34/39] mark new version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09c65689e..0d614e20c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "poke_env" -version = "0.8.3.1" +version = "0.8.3.2" description = "A python interface for training Reinforcement Learning bots to battle on pokemon showdown." readme = "README.md" requires-python = ">=3.9.0" From 5b5c168bef6915429c2900ef294459988d10ec30 Mon Sep 17 00:00:00 2001 From: jakegrigsby Date: Sun, 6 Jul 2025 03:40:10 -0500 Subject: [PATCH 35/39] revival blessing --- src/poke_env/environment/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poke_env/environment/battle.py b/src/poke_env/environment/battle.py index 9c22b2176..558df9846 100644 --- a/src/poke_env/environment/battle.py +++ b/src/poke_env/environment/battle.py @@ -126,7 +126,7 @@ def parse_request(self, request: Dict[str, Any]) -> None: if not self.trapped and self.reviving: for pokemon in side["pokemon"]: - if pokemon and pokemon.get("reviving", False): + if pokemon and not pokemon.get("reviving", False): pokemon = self._team[pokemon["ident"]] if not pokemon.active: self._available_switches.append(pokemon) From 7c6b83b3d49a32550d0aa03308b37f79784390f5 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Thu, 10 Jul 2025 14:34:26 -0500 Subject: [PATCH 36/39] add basic sleep_between throttle for player laddering --- src/poke_env/player/player.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index b6f1f7851..72fcfeec4 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -1,6 +1,7 @@ """This module defines a base class for players.""" import asyncio +import time import random from abc import ABC, abstractmethod from asyncio import Condition, Event, Queue, Semaphore @@ -659,21 +660,23 @@ def choose_random_move(battle: AbstractBattle) -> BattleOrder: f"battle should be Battle or DoubleBattle. Received {type(battle)}" ) - async def ladder(self, n_games: int): + async def ladder(self, n_games: int, sleep_between: Optional[int] = None): """Make the player play games on the ladder. n_games defines how many battles will be played. :param n_games: Number of battles that will be played :type n_games: int + :param sleep_between: Seconds to wait before challenging again + :type sleep_between: int """ - await handle_threaded_coroutines(self._ladder(n_games)) + await handle_threaded_coroutines(self._ladder(n_games, sleep_between=sleep_between)) - async def _ladder(self, n_games: int): + async def _ladder(self, n_games: int, sleep_between: Optional[int] = None): await self.ps_client.logged_in.wait() start_time = perf_counter() - for _ in range(n_games): + for game_num in range(n_games): async with self._battle_start_condition: await self.ps_client.search_ladder_game(self._format, self.next_team) await self._battle_start_condition.wait() @@ -681,6 +684,10 @@ async def _ladder(self, n_games: int): async with self._battle_end_condition: await self._battle_end_condition.wait() await self._battle_semaphore.acquire() + if game_num < n_games - 1 and sleep_between is not None: + print("sleeping between games...") + time.sleep(sleep_between) + print("...done sleeping") await self._battle_count_queue.join() self.logger.info( "Laddering (%d battles) finished in %fs", From 701216c726cff7ef2db73a97b05d496010e4c154 Mon Sep 17 00:00:00 2001 From: jakegrigsby Date: Fri, 11 Jul 2025 03:38:07 -0500 Subject: [PATCH 37/39] basic throttle pt --- src/poke_env/player/openai_api.py | 10 ++++++++-- src/poke_env/player/player.py | 4 +--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/poke_env/player/openai_api.py b/src/poke_env/player/openai_api.py index 0d726fddb..4c25f198b 100644 --- a/src/poke_env/player/openai_api.py +++ b/src/poke_env/player/openai_api.py @@ -529,26 +529,32 @@ async def _ladder_loop( self, n_challenges: Optional[int] = None, callback: Optional[Callable[[AbstractBattle], None]] = None, + sleep_between: Optional[int] = None, ): if n_challenges: if n_challenges <= 0: raise ValueError( f"Number of challenges must be > 0. Got {n_challenges}" ) - for _ in range(n_challenges): + for game_num in range(n_challenges): await self.agent.ladder(1) if callback and self.current_battle is not None: callback(self.current_battle) + if game_num < n_challenges - 1 and sleep_between is not None: + await asyncio.sleep(random.randint(0, sleep_between)) else: while self._keep_challenging: await self.agent.ladder(1) if callback and self.current_battle is not None: callback(self.current_battle) + if sleep_between is not None: + await asyncio.sleep(random.randint(0, sleep_between)) def start_laddering( self, n_challenges: Optional[int] = None, callback: Optional[Callable[[AbstractBattle], None]] = None, + sleep_between: Optional[int] = None, ): """ Starts the laddering loop. @@ -570,7 +576,7 @@ def start_laddering( if not n_challenges: self._keep_challenging = True self._challenge_task = asyncio.run_coroutine_threadsafe( - self._ladder_loop(n_challenges, callback), POKE_LOOP + self._ladder_loop(n_challenges, callback, sleep_between=sleep_between), POKE_LOOP ) async def _stop_challenge_loop( diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index 72fcfeec4..93eaa69a9 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -685,9 +685,7 @@ async def _ladder(self, n_games: int, sleep_between: Optional[int] = None): await self._battle_end_condition.wait() await self._battle_semaphore.acquire() if game_num < n_games - 1 and sleep_between is not None: - print("sleeping between games...") - time.sleep(sleep_between) - print("...done sleeping") + await asyncio.sleep(random.randint(0, sleep_between)) await self._battle_count_queue.join() self.logger.info( "Laddering (%d battles) finished in %fs", From cde9ac99cea5783e3ac289602e3d3f60dd06fa4d Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Thu, 17 Jul 2025 00:44:22 -0500 Subject: [PATCH 38/39] improve n_won_battles crash during long-term self-play --- src/poke_env/player/player.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index 72fcfeec4..fddbf82be 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -854,11 +854,13 @@ def format_is_doubles(self) -> bool: @property def n_finished_battles(self) -> int: - return len([None for b in self._battles.values() if b.finished]) + battles = list(self._battles.values()) + return len([None for b in battles if b.finished]) @property def n_lost_battles(self) -> int: - return len([None for b in self._battles.values() if b.lost]) + battles = list(self._battles.values()) + return len([None for b in battles if b.lost]) @property def n_tied_battles(self) -> int: @@ -866,7 +868,8 @@ def n_tied_battles(self) -> int: @property def n_won_battles(self) -> int: - return len([None for b in self._battles.values() if b.won]) + battles = list(self._battles.values()) + return len([None for b in battles if b.won]) @property def accept_open_team_sheet(self) -> bool: From 3b11ebf282ac79d2e5eb8739ebdc83ed31d5c320 Mon Sep 17 00:00:00 2001 From: Jake Grigsby Date: Fri, 25 Jul 2025 14:31:40 -0500 Subject: [PATCH 39/39] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b420a11e4..7d1a98cd8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ >[!IMPORTANT] > This is a fork of [poke-env](https://github.com/hsahovic/poke-env) that installs with [`metamon`](https://github.com/UT-Austin-RPL/metamon). It attempts to extend the lifespan of poke-env as it was during Metamon's development: -> 1. Maintains the original gymnasium interface that existed until v0.8.3. `OpenAIGymEnv` (+ ability to swap in custom Players). Rewards functions that take `last_battle` and `current_battle` as input (+ a speed bost). Removes "observation" system that slows fps and is already handled by Metamon. +> 1. Maintains the original gymnasium interface that existed until v0.8.3. `OpenAIGymEnv` (+ ability to swap in custom Players). Rewards functions that take `last_battle` and `current_battle` as input (+ a speed boost). Removes "observation" system that slows fps and is already handled by Metamon. > 2. Preserves minor early-generation battle details as they were when Metamon's original models were trained. > 3. Tries to bring key fixes/improvements since v0.8.3 that are unrelated to gymnasium. >