diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py index 35b05f630..d0c47008f 100644 --- a/src/poke_env/environment/abstract_battle.py +++ b/src/poke_env/environment/abstract_battle.py @@ -449,8 +449,8 @@ def parse_message(self, split_message: List[str]): if event[-1].startswith("[anim]"): event = event[:-1] - if event[-1].startswith("[from]move: "): - override_move = event.pop()[12:] + if event[-1].startswith("[from] move: "): + override_move = event.pop()[13:] if override_move == "Sleep Talk": # Sleep talk was used, but also reveals another move @@ -461,7 +461,7 @@ def parse_message(self, split_message: List[str]): override_move = None elif self.logger is not None: self.logger.warning( - "Unmanaged [from]move message received - move %s in cleaned up " + "Unmanaged [from] move message received - move %s in cleaned up " "message %s in battle %s turn %d", override_move, event, @@ -472,8 +472,8 @@ def parse_message(self, split_message: List[str]): if event[-1] == "null": event = event[:-1] - if event[-1].startswith("[from]ability: "): - revealed_ability = event.pop()[15:] + if event[-1].startswith("[from] ability: "): + revealed_ability = event.pop()[16:] pokemon = event[2] self.get_pokemon(pokemon).ability = revealed_ability @@ -483,7 +483,7 @@ def parse_message(self, split_message: List[str]): return elif self.logger is not None: self.logger.warning( - "Unmanaged [from]ability: message received - ability %s in " + "Unmanaged [from] ability: message received - ability %s in " "cleaned up message %s in battle %s turn %d", revealed_ability, event, diff --git a/src/poke_env/environment/battle.py b/src/poke_env/environment/battle.py index 655baf728..5f77f3fc1 100644 --- a/src/poke_env/environment/battle.py +++ b/src/poke_env/environment/battle.py @@ -94,6 +94,16 @@ def parse_request(self, request: Dict[str, Any]) -> None: self._teampreview = False self._update_team_from_request(request["side"]) + if self.active_pokemon is not None: + active_mon = self.get_pokemon( + request["side"]["pokemon"][0]["ident"], + force_self_team=True, + details=request["side"]["pokemon"][0]["details"], + ) + if active_mon != self.active_pokemon: + self.active_pokemon.switch_out() + active_mon.switch_in() + if "active" in request: active_request = request["active"][0] diff --git a/src/poke_env/environment/double_battle.py b/src/poke_env/environment/double_battle.py index 30b3258bd..35f7e44a1 100644 --- a/src/poke_env/environment/double_battle.py +++ b/src/poke_env/environment/double_battle.py @@ -130,6 +130,13 @@ def parse_request(self, request: Dict[str, Any]) -> None: if side["pokemon"]: self._player_role = side["pokemon"][0]["ident"][:2] self._update_team_from_request(side) + if self.player_role is not None: + self._active_pokemon[f"{self.player_role}a"] = self.team[ + request["side"]["pokemon"][0]["ident"] + ] + self._active_pokemon[f"{self.player_role}b"] = self.team[ + request["side"]["pokemon"][1]["ident"] + ] if "active" in request: for active_pokemon_number, active_request in enumerate(request["active"]): @@ -139,41 +146,6 @@ def parse_request(self, request: Dict[str, Any]) -> None: force_self_team=True, details=pokemon_dict["details"], ) - if self.player_role is not None: - if ( - active_pokemon_number == 0 - and f"{self.player_role}a" not in self._active_pokemon - ): - self._active_pokemon[f"{self.player_role}a"] = active_pokemon - elif f"{self.player_role}b" not in self._active_pokemon: - self._active_pokemon[f"{self.player_role}b"] = active_pokemon - elif ( - active_pokemon_number == 0 - and self._active_pokemon[f"{self.player_role}a"].fainted - and self._active_pokemon[f"{self.player_role}b"] - == active_pokemon - ): - ( - self._active_pokemon[f"{self.player_role}a"], - self._active_pokemon[f"{self.player_role}b"], - ) = ( - self._active_pokemon[f"{self.player_role}b"], - self._active_pokemon[f"{self.player_role}a"], - ) - elif ( - active_pokemon_number == 1 - and self._active_pokemon[f"{self.player_role}b"].fainted - and not active_pokemon.fainted - and self._active_pokemon[f"{self.player_role}a"] - == active_pokemon - ): - ( - self._active_pokemon[f"{self.player_role}a"], - self._active_pokemon[f"{self.player_role}b"], - ) = ( - self._active_pokemon[f"{self.player_role}b"], - self._active_pokemon[f"{self.player_role}a"], - ) if active_pokemon.fainted: continue diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py index ecb75575f..ca16ea3f3 100644 --- a/src/poke_env/environment/pokemon.py +++ b/src/poke_env/environment/pokemon.py @@ -378,6 +378,7 @@ def set_hp_status(self, hp_status: str, store=False): self.end_effect("yawn") else: hp = hp_status + self._status = None current_hp, max_hp = "".join([c for c in hp if c in "0123456789/"]).split("/") self._current_hp = int(current_hp) diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py index fd26f9eb7..f084974fb 100644 --- a/src/poke_env/player/player.py +++ b/src/poke_env/player/player.py @@ -9,7 +9,7 @@ from asyncio import Condition, Event, Queue, Semaphore from logging import Logger from time import perf_counter -from typing import Any, Awaitable, Dict, List, Optional, Union +from typing import Any, Awaitable, Dict, List, Optional, Tuple, Union import orjson @@ -146,6 +146,7 @@ def __init__( self._battles: Dict[str, AbstractBattle] = {} self._battle_semaphore: Semaphore = create_in_poke_loop(Semaphore, 0) + self._reqs: Dict[str, List[List[str]]] = {} self._battle_start_condition: Condition = create_in_poke_loop(Condition) self._battle_count_queue: Queue[Any] = create_in_poke_loop( @@ -264,119 +265,153 @@ async def _handle_battle_message(self, split_messages: List[List[str]]): :type split_message: str """ # Battle messages can be multiline + battle_tag = split_messages[0][0] if ( len(split_messages) > 1 and len(split_messages[1]) > 1 and split_messages[1][1] == "init" ): - battle_info = split_messages[0][0].split("-") + battle_info = battle_tag.split("-") battle = await self._create_battle(battle_info) else: - battle = await self._get_battle(split_messages[0][0]) - - for split_message in split_messages[1:]: - if len(split_message) <= 1: - continue - elif split_message[1] == "": - battle.parse_message(split_message) - elif split_message[1] in self.MESSAGES_TO_IGNORE: - pass - elif split_message[1] == "request": - if split_message[2]: - request = orjson.loads(split_message[2]) - battle.parse_request(request) - if battle.move_on_next_request: - 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]) - else: - battle.tied() - await self._battle_count_queue.get() - self._battle_count_queue.task_done() - self._battle_finished_callback(battle) - async with self._battle_end_condition: - self._battle_end_condition.notify_all() - elif split_message[1] == "error": - self.logger.log( - 25, "Error message received: %s", "|".join(split_message) + battle = await self._get_battle(battle_tag) + request = self._reqs.pop(battle_tag, None) + if split_messages[1][1] == "request": + protocol = None + self._reqs[battle_tag] = split_messages + else: + protocol = split_messages + if protocol is not None or request is not None: + split_messages = protocol or [[f">{battle_tag}"]] + if request is not None: + split_messages += [request[1]] + results = [ + await self._process_split_message(m, battle) for m in split_messages + ] + should_process_request = any([r[0] for r in results]) + is_from_teampreview = any([r[1] for r in results]) + should_maybe_default = any([r[2] for r in results]) + if should_process_request: + await self._handle_battle_request( + battle, + from_teampreview_request=is_from_teampreview, + maybe_default_order=should_maybe_default, ) - if split_message[2].startswith( - "[Invalid choice] Sorry, too late to make a different move" - ): - if battle.trapped: - await self._handle_battle_request(battle) - 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) - elif split_message[2].startswith( - "[Invalid choice] Can't switch: You can't switch to an active " - "Pokémon" - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Can't switch: You can't switch to a fainted " - "Pokémon" - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Can't move: Invalid target for" - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Can't move: You can't choose a target for" - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Can't move: " - ) and split_message[2].endswith("needs a target"): - await self._handle_battle_request(battle, maybe_default_order=True) - elif ( - split_message[2].startswith("[Invalid choice] Can't move: Your") - and " doesn't have a move matching " in split_message[2] - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Incomplete choice: " - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Unavailable choice]" - ) and split_message[2].endswith("is disabled"): - battle.move_on_next_request = True - elif split_message[2].startswith("[Invalid choice]") and split_message[ - 2 - ].endswith("is disabled"): - battle.move_on_next_request = True - elif split_message[2].startswith( - "[Invalid choice] Can't move: You sent more choices than unfainted" - " Pokémon." - ): - await self._handle_battle_request(battle, maybe_default_order=True) - elif split_message[2].startswith( - "[Invalid choice] Can't move: You can only Terastallize once per battle." - ): - 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": - await self._handle_ots_request(battle.battle_tag) + + async def _process_split_message( + self, split_message: List[str], battle: AbstractBattle + ) -> Tuple[bool, bool, bool]: + should_process_request = False + is_from_teampreview = False + should_maybe_default = False + if len(split_message) <= 1: + pass + elif split_message[1] == "": + battle.parse_message(split_message) + elif split_message[1] in self.MESSAGES_TO_IGNORE: + pass + elif split_message[1] == "request": + if split_message[2]: + request = orjson.loads(split_message[2]) + battle.parse_request(request) + if battle.move_on_next_request: + should_process_request = True + 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]) else: - battle.parse_message(split_message) + battle.tied() + await self._battle_count_queue.get() + self._battle_count_queue.task_done() + self._battle_finished_callback(battle) + async with self._battle_end_condition: + self._battle_end_condition.notify_all() + elif split_message[1] == "error": + self.logger.log(25, "Error message received: %s", "|".join(split_message)) + if split_message[2].startswith( + "[Invalid choice] Sorry, too late to make a different move" + ): + if battle.trapped: + should_process_request = True + 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 + should_process_request = True + elif split_message[2].startswith( + "[Invalid choice] Can't switch: You can't switch to an active " + "Pokémon" + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith( + "[Invalid choice] Can't switch: You can't switch to a fainted " + "Pokémon" + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith( + "[Invalid choice] Can't move: Invalid target for" + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith( + "[Invalid choice] Can't move: You can't choose a target for" + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith( + "[Invalid choice] Can't move: " + ) and split_message[2].endswith("needs a target"): + should_process_request = True + should_maybe_default = True + elif ( + split_message[2].startswith("[Invalid choice] Can't move: Your") + and " doesn't have a move matching " in split_message[2] + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith("[Invalid choice] Incomplete choice: "): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith("[Unavailable choice]") and split_message[ + 2 + ].endswith("is disabled"): + battle.move_on_next_request = True + elif split_message[2].startswith("[Invalid choice]") and split_message[ + 2 + ].endswith("is disabled"): + battle.move_on_next_request = True + elif split_message[2].startswith( + "[Invalid choice] Can't move: You sent more choices than unfainted" + " Pokémon." + ): + should_process_request = True + should_maybe_default = True + elif split_message[2].startswith( + "[Invalid choice] Can't move: You can only Terastallize once per battle." + ): + should_process_request = True + should_maybe_default = True + else: + self.logger.critical("Unexpected error message: %s", split_message) + elif split_message[1] == "turn": + battle.parse_message(split_message) + should_process_request = True + elif split_message[1] == "teampreview": + battle.parse_message(split_message) + should_process_request = True + is_from_teampreview = 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": + await self._handle_ots_request(battle.battle_tag) + else: + battle.parse_message(split_message) + return should_process_request, is_from_teampreview, should_maybe_default async def _handle_battle_request( self, @@ -532,9 +567,7 @@ def choose_random_doubles_move(battle: DoubleBattle) -> BattleOrder: second_switch_in = random.choice(available_switches) second_order = BattleOrder(second_switch_in) - if first_order and second_order: - return DoubleBattleOrder(first_order, second_order) - return DoubleBattleOrder(first_order or second_order, None) + return DoubleBattleOrder(first_order, second_order) for ( orders, diff --git a/unit_tests/environment/test_battle.py b/unit_tests/environment/test_battle.py index dafb74318..951b3b241 100644 --- a/unit_tests/environment/test_battle.py +++ b/unit_tests/environment/test_battle.py @@ -426,10 +426,16 @@ def test_battle_request_and_interactions(example_request): ) assert "precipiceblades" in battle.opponent_active_pokemon.moves - event = ["", "move", "p1: Groudon", "Teeter Dance", "[from]ability: Dancer"] + event = ["", "move", "p1: Groudon", "Teeter Dance", "[from] ability: Dancer"] battle.parse_message(event) assert "teeterdance" not in battle.opponent_active_pokemon.moves - assert event == ["", "move", "p1: Groudon", "Teeter Dance", "[from]ability: Dancer"] + assert event == [ + "", + "move", + "p1: Groudon", + "Teeter Dance", + "[from] ability: Dancer", + ] battle._player_username = "ray" battle._opponent_username = "wolfe" diff --git a/unit_tests/environment/test_double_battle.py b/unit_tests/environment/test_double_battle.py index c4711c231..7dfdde6a5 100644 --- a/unit_tests/environment/test_double_battle.py +++ b/unit_tests/environment/test_double_battle.py @@ -373,7 +373,7 @@ def test_pledge_moves(): "p2a: Primarina", "Water Pledge", "p1b: Hatterene", - "[from]move: Grass Pledge", + "[from] move: Grass Pledge", ], ["", "-combine"], ["", "-damage", "p1b: Hatterene", "0 fnt"],