Repository: hsahovic/poke-env

Issue:
-singlemove handling
In battle.parse_message handling, we erroneously ignore -singlemove, so we don't record effects like Destiny Bond/Glaive Rush/Grudge/Rage. I plan to submit a PR to just remove this from MESSAGES_TO_IGNORE and also add a test

Hints from issue comments:
@hsahovic: Fixed by https://github.com/hsahovic/poke-env/pull/633
Thanks @caymansimpson !

Solution patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 8612b2ef9..d0010d81a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -45,8 +45,8 @@ jobs:
       - name: Run isort
         run: isort --check .
 
-      - name: Run pyright
-        run: pyright src/
+      - name: Run mypy
+        run: mypy src
 
       - name: Build docs
         run: |
diff --git a/examples/requirements.txt b/examples/requirements.txt
index 98ac2be7d..3a71e470c 100644
--- a/examples/requirements.txt
+++ b/examples/requirements.txt
@@ -1,2 +1,2 @@
-tensorflow<2.18
+tensorflow<2.19
 keras-rl2
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 3c938dd98..eca3e686d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,9 +1,9 @@
 black==24.8.0
 flake8
 isort==5.13.2
+mypy
 nbsphinx
 pre-commit
-pyright==1.1.379
 pytest
 pytest-asyncio
 pytest-cov
@@ -11,4 +11,5 @@ pytest-timeout
 sphinx
 sphinx_rtd_theme
 twine
-wheel
\ No newline at end of file
+types-requests
+wheel
diff --git a/requirements.txt b/requirements.txt
index bb82334a9..75f2fa17b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,4 @@ numpy
 orjson
 requests
 tabulate
-websockets>=8.0
+websockets==12.0
diff --git a/src/poke_env/data/static/learnset.json b/src/poke_env/data/static/learnset.json
index af2066999..587d4bfed 100644
--- a/src/poke_env/data/static/learnset.json
+++ b/src/poke_env/data/static/learnset.json
@@ -67280,7 +67280,7 @@
    "drainpunch": ["9M", "7T", "6T", "5T"],
    "dreameater": ["7M", "6M", "5M"],
    "dualchop": ["7T", "6T", "5T"],
-   "echoedvoice": ["9L36", "7M", "7L36", "6M", "6L36", "5M", "5L36"],
+   "echoedvoice": ["9L36", "9S5", "7M", "7L36", "6M", "6L36", "5M", "5L36"],
    "embargo": ["7M", "6M", "5M"],
    "endure": ["9M"],
    "energyball": ["9M", "7M", "6M", "5M"],
@@ -67316,14 +67316,14 @@
    "playrough": ["9M"],
    "poweruppunch": ["6M"],
    "protect": ["9M", "7M", "6M", "5M"],
-   "psybeam": ["9M", "9L31", "7L31", "6L31", "5L31"],
+   "psybeam": ["9M", "9L31", "9S5", "7L31", "6L31", "5L31"],
    "psychic": ["9M", "9L57", "9S4", "7M", "7L57", "7S2", "6M", "6L57", "5M", "5L57", "5S1"],
    "psychup": ["9M", "7M", "6M", "5M"],
    "psyshock": ["9M", "7M", "6M", "5M"],
    "quickattack": ["9L1", "7L1", "6L6", "5L6", "5S0"],
    "raindance": ["9M", "7M", "6M", "5M"],
    "recycle": ["7T", "6T", "5T"],
-   "relicsong": ["9L50", "9S4", "7T", "7S3", "6T", "5T"],
+   "relicsong": ["9L50", "9S4", "9S5", "7T", "7S3", "6T", "5T"],
    "rest": ["9M", "7M", "6M", "5M"],
    "retaliate": ["6M", "5M"],
    "return": ["7M", "6M", "5M"],
@@ -67337,7 +67337,7 @@
    "shadowclaw": ["9M", "7M", "6M", "5M"],
    "shockwave": ["7T", "6T"],
    "signalbeam": ["7T", "6T", "5T"],
-   "sing": ["9L1", "9S4", "7L1", "7S2", "7S3", "6L16", "5L16"],
+   "sing": ["9L1", "9S4", "9S5", "7L1", "7S2", "7S3", "6L16", "5L16"],
    "skillswap": ["9M", "7T", "6T", "5T"],
    "sleeptalk": ["9M", "7M", "6M", "5T"],
    "snatch": ["7T", "6T", "5T"],
@@ -67372,7 +67372,8 @@
    {"generation": 5, "level": 50, "moves": ["round", "teeterdance", "psychic", "closecombat"], "pokeball": "cherishball"},
    {"generation": 7, "level": 15, "moves": ["sing", "psychic", "closecombat"], "pokeball": "cherishball"},
    {"generation": 7, "level": 50, "moves": ["sing", "celebrate", "round", "relicsong"], "pokeball": "cherishball"},
-   {"generation": 9, "level": 70, "moves": ["relicsong", "hypervoice", "sing", "psychic"]}
+   {"generation": 9, "level": 70, "moves": ["relicsong", "hypervoice", "sing", "psychic"]},
+   {"generation": 9, "level": 50, "shiny": true, "nature": "Modest", "ivs": {"hp": 20, "atk": 20, "def": 20, "spa": 31, "spd": 31, "spe": 31}, "moves": ["relicsong", "echoedvoice", "psybeam", "sing"], "pokeball": "cherishball"}
   ],
   "eventOnly": true
  },
diff --git a/src/poke_env/data/static/moves/gen7moves.json b/src/poke_env/data/static/moves/gen7moves.json
index 5b6544908..90dd6822c 100644
--- a/src/poke_env/data/static/moves/gen7moves.json
+++ b/src/poke_env/data/static/moves/gen7moves.json
@@ -14864,6 +14864,7 @@
         "category": "Status",
         "condition": {
             "duration": 1,
+            "noCopy": true,
             "onFoeRedirectTarget": "onFoeRedirectTarget",
             "onFoeRedirectTargetPriority": 2,
             "onStart": "onStart"
diff --git a/src/poke_env/data/static/moves/gen8moves.json b/src/poke_env/data/static/moves/gen8moves.json
index c958ccfdd..f3ecd7b78 100644
--- a/src/poke_env/data/static/moves/gen8moves.json
+++ b/src/poke_env/data/static/moves/gen8moves.json
@@ -17354,6 +17354,7 @@
         "category": "Status",
         "condition": {
             "duration": 1,
+            "noCopy": true,
             "onFoeRedirectTarget": "onFoeRedirectTarget",
             "onFoeRedirectTargetPriority": 2,
             "onStart": "onStart"
diff --git a/src/poke_env/data/static/moves/gen9moves.json b/src/poke_env/data/static/moves/gen9moves.json
index be76ec3c9..4bb1e907f 100644
--- a/src/poke_env/data/static/moves/gen9moves.json
+++ b/src/poke_env/data/static/moves/gen9moves.json
@@ -18518,6 +18518,7 @@
         "category": "Status",
         "condition": {
             "duration": 1,
+            "noCopy": true,
             "onFoeRedirectTarget": "onFoeRedirectTarget",
             "onFoeRedirectTargetPriority": 2,
             "onStart": "onStart"
diff --git a/src/poke_env/environment/abstract_battle.py b/src/poke_env/environment/abstract_battle.py
index 6dd86fb9b..eb71cb81d 100644
--- a/src/poke_env/environment/abstract_battle.py
+++ b/src/poke_env/environment/abstract_battle.py
@@ -31,7 +31,6 @@ class AbstractBattle(ABC):
         "-nothing",
         "-ohko",
         "-resisted",
-        "-singlemove",
         "-supereffective",
         "-waiting",
         "-zbroken",
@@ -163,6 +162,9 @@ def __init__(
         self.rules: List[str] = []
         self._turn: int = 0
         self._opponent_can_terrastallize: bool = True
+        self._opponent_can_mega_evolve: Union[bool, List[bool]] = True
+        self._opponent_can_z_move: Union[bool, List[bool]] = True
+
         self._opponent_dynamax_turn: Optional[int] = None
         self._opponent_rating: Optional[int] = None
         self._rating: Optional[int] = None
@@ -224,7 +226,7 @@ def get_pokemon(
         if is_mine or force_self_team:
             team: Dict[str, Pokemon] = self._team
         else:
-            team: Dict[str, Pokemon] = self._opponent_team
+            team: Dict[str, Pokemon] = self._opponent_team  # type: ignore
 
         if self._team_size and len(team) >= self._team_size[player_role]:
             raise ValueError(
@@ -330,8 +332,8 @@ def _end_illusion_on(
             return illusionist_mon
 
         illusionist_mon.switch_in(details=details)
-        illusionist_mon.status = (
-            illusioned.status.name if illusioned.status is not None else None
+        illusionist_mon.status = (  # type: ignore
+            illusioned.status if illusioned.status is not None else None
         )
         illusionist_mon.set_hp(f"{illusioned.current_hp}/{illusioned.max_hp}")
 
@@ -598,10 +600,10 @@ def parse_message(self, split_message: List[str]):
             self.get_pokemon(pokemon).ability = ability
         elif split_message[1] == "-start":
             pokemon, effect = split_message[2:4]
-            pokemon = self.get_pokemon(pokemon)
-            pokemon.start_effect(effect)
+            pokemon = self.get_pokemon(pokemon)  # type: ignore
+            pokemon.start_effect(effect)  # type: ignore
 
-            if pokemon.is_dynamaxed:
+            if pokemon.is_dynamaxed:  # type: ignore
                 if pokemon in set(self.team.values()) and self._dynamax_turn is None:
                     self._dynamax_turn = self.turn
                 # self._can_dynamax value is set via _parse_request()
@@ -617,7 +619,7 @@ def parse_message(self, split_message: List[str]):
                 self.get_pokemon(target).start_effect(effect)
         elif split_message[1] == "-status":
             pokemon, status = split_message[2:4]
-            self.get_pokemon(pokemon).status = status
+            self.get_pokemon(pokemon).status = status  # type: ignore
         elif split_message[1] == "rule":
             self.rules.append(split_message[2])
 
@@ -682,13 +684,13 @@ def parse_message(self, split_message: List[str]):
         elif split_message[1] == "-prepare":
             try:
                 attacker, move, defender = split_message[2:5]
-                defender = self.get_pokemon(defender)
+                defender_mon = self.get_pokemon(defender)
                 if to_id_str(move) == "skydrop":
-                    defender.start_effect("Sky Drop")
+                    defender_mon.start_effect("Sky Drop")
             except ValueError:
                 attacker, move = split_message[2:4]
-                defender = None
-            self.get_pokemon(attacker).prepare(move, defender)
+                defender_mon = None
+            self.get_pokemon(attacker).prepare(move, defender_mon)
         elif split_message[1] == "-primal":
             pokemon = split_message[2]
             self.get_pokemon(pokemon).primal()
@@ -709,12 +711,12 @@ def parse_message(self, split_message: List[str]):
             self.get_pokemon(pokemon).start_effect(effect.replace("move: ", ""))
         elif split_message[1] == "-swapboost":
             source, target, stats = split_message[2:5]
-            source = self.get_pokemon(source)
-            target = self.get_pokemon(target)
+            source_mon = self.get_pokemon(source)
+            target_mon = self.get_pokemon(target)
             for stat in stats.split(", "):
-                source.boosts[stat], target.boosts[stat] = (
-                    target.boosts[stat],
-                    source.boosts[stat],
+                source_mon.boosts[stat], target_mon.boosts[stat] = (
+                    target_mon.boosts[stat],
+                    source_mon.boosts[stat],
                 )
         elif split_message[1] == "-transform":
             pokemon, into = split_message[2:4]
@@ -785,11 +787,11 @@ def parse_message(self, split_message: List[str]):
             self._register_teampreview_pokemon(player, details)
         elif split_message[1] == "raw":
             username, rating_info = split_message[2].split("'s rating: ")
-            rating = int(rating_info[:4])
+            rating_int = int(rating_info[:4])
             if username == self.player_username:
-                self._rating = rating
+                self._rating = rating_int
             elif username == self.opponent_username:
-                self._opponent_rating = rating
+                self._opponent_rating = rating_int
             elif self.logger is not None:
                 self.logger.warning(
                     "Rating information regarding an unrecognized username received. "
@@ -809,18 +811,17 @@ def parse_message(self, split_message: List[str]):
             self._swap(pokemon, position)  # type: ignore
         elif split_message[1] == "teamsize":
             player, number = split_message[2:4]
-            number = int(number)
-            self._team_size[player] = number
+            self._team_size[player] = int(number)
         elif split_message[1] in {"message", "-message"}:
             if self.logger is not None:
                 self.logger.info("Received message: %s", split_message[2])
         elif split_message[1] == "-immune":
             if len(split_message) == 4:
-                mon, cause = split_message[2:]
+                mon, cause = split_message[2:]  # type: ignore
 
                 if cause.startswith("[from] ability:"):
                     ability = cause.replace("[from] ability:", "")
-                    self.get_pokemon(mon).ability = to_id_str(ability)
+                    self.get_pokemon(mon).ability = to_id_str(ability)  # type: ignore
         elif split_message[1] == "-swapsideconditions":
             self._side_conditions, self._opponent_side_conditions = (
                 self._opponent_side_conditions,
@@ -831,10 +832,10 @@ def parse_message(self, split_message: List[str]):
             self.players = player_1, player_2
         elif split_message[1] == "-terastallize":
             pokemon, type_ = split_message[2:]
-            pokemon = self.get_pokemon(pokemon)
-            pokemon.terastallize(type_)
+            pokemon = self.get_pokemon(pokemon)  # type: ignore
+            pokemon.terastallize(type_)  # type: ignore
 
-            if pokemon.is_terastallized:
+            if pokemon.is_terastallized:  # type: ignore
                 if pokemon in set(self.opponent_team.values()):
                     self._opponent_can_terrastallize = False
         else:
@@ -961,6 +962,7 @@ def dynamax_turns_left(self) -> Optional[int]:
             map(lambda pokemon: pokemon.is_dynamaxed, self._team.values())
         ):
             return max(3 - (self.turn - self._dynamax_turn), 0)
+        return None
 
     @property
     def fields(self) -> Dict[Field, int]:
@@ -1059,6 +1061,7 @@ def opponent_dynamax_turns_left(self) -> Optional[int]:
             map(lambda pokemon: pokemon.is_dynamaxed, self._opponent_team.values())
         ):
             return max(3 - (self.turn - self._opponent_dynamax_turn), 0)
+        return None
 
     @property
     def opponent_role(self) -> Optional[str]:
@@ -1070,6 +1073,7 @@ def opponent_role(self) -> Optional[str]:
             return "p2"
         if self.player_role == "p2":
             return "p1"
+        return None
 
     @property
     def opponent_side_conditions(self) -> Dict[SideCondition, int]:
diff --git a/src/poke_env/environment/battle.py b/src/poke_env/environment/battle.py
index f0cdc92de..e8f33c8d9 100644
--- a/src/poke_env/environment/battle.py
+++ b/src/poke_env/environment/battle.py
@@ -159,6 +159,7 @@ def active_pokemon(self) -> Optional[Pokemon]:
         for pokemon in self.team.values():
             if pokemon.active:
                 return pokemon
+        return None
 
     @property
     def all_active_pokemons(self) -> List[Optional[Pokemon]]:
@@ -258,7 +259,7 @@ def opponent_can_dynamax(self, value: bool):
         self._opponent_can_dynamax = value
 
     @property
-    def opponent_can_mega_evolve(self) -> bool:
+    def opponent_can_mega_evolve(self) -> Union[bool, List[bool]]:
         """
         :return: Whether or not opponent's current active pokemon can mega-evolve
         :rtype: bool
@@ -278,7 +279,7 @@ def opponent_can_tera(self) -> bool:
         return self._opponent_can_tera
 
     @property
-    def opponent_can_z_move(self) -> bool:
+    def opponent_can_z_move(self) -> Union[bool, List[bool]]:
         """
         :return: Whether or not opponent's current active pokemon can z-move
         :rtype: bool
diff --git a/src/poke_env/environment/move.py b/src/poke_env/environment/move.py
index 9a0ca6920..c1352beb8 100644
--- a/src/poke_env/environment/move.py
+++ b/src/poke_env/environment/move.py
@@ -96,8 +96,7 @@ def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None):
 
             if base_power:
                 try:
-                    base_power = int(base_power)
-                    self._base_power_override = base_power
+                    self._base_power_override = int(base_power)
                 except ValueError:
                     pass
 
diff --git a/src/poke_env/environment/observed_pokemon.py b/src/poke_env/environment/observed_pokemon.py
index aaa5ef57e..b60398e48 100644
--- a/src/poke_env/environment/observed_pokemon.py
+++ b/src/poke_env/environment/observed_pokemon.py
@@ -59,7 +59,9 @@ def from_pokemon(mon: Pokemon):
         if mon is None:
             return None
 
-        stats = ObservedPokemon.initial_stats()
+        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()}
 
diff --git a/src/poke_env/environment/pokemon.py b/src/poke_env/environment/pokemon.py
index 1ac27d588..999286087 100644
--- a/src/poke_env/environment/pokemon.py
+++ b/src/poke_env/environment/pokemon.py
@@ -73,7 +73,6 @@ def __init__(
 
         # Individual related attributes
         self._ability: Optional[str] = None
-        self._active: bool
         self._gender: Optional[PokemonGender] = None
         self._level: int = 100
         self._max_hp: Optional[int] = 0
@@ -102,7 +101,7 @@ def __init__(
         self._last_details: str = ""
         self._must_recharge: bool = False
         self._preparing_move: Optional[Move] = None
-        self._preparing_target = None
+        self._preparing_target: Optional[bool | Pokemon] = None
         self._protect_counter: int = 0
         self._revealed: bool = False
         self._stats: Dict[str, Optional[int]] = {
@@ -144,7 +143,7 @@ def _add_move(self, move_id: str, use: bool = False) -> Optional[Move]:
         id_ = Move.retrieve_id(move_id)
 
         if not Move.should_be_stored(id_, self._data.gen):
-            return
+            return None
 
         if id_ not in self._moves:
             move = Move(move_id=id_, raw_id=move_id, gen=self._data.gen)
@@ -273,11 +272,11 @@ def moved(self, move_id: str, failed: bool = False, use: bool = True):
                     move_id: m for move_id, m in self._moves.items() if m is move
                 }
 
-            for move in self._moves:
+            for move_name in self._moves:
                 if len(new_moves) == 4:
                     break
-                elif move not in new_moves:
-                    new_moves[move] = self._moves[move]
+                elif move_name not in new_moves:
+                    new_moves[move_name] = self._moves[move_name]
 
             self._moves = new_moves
 
@@ -334,9 +333,9 @@ def set_hp_status(self, hp_status: str, store=False):
         else:
             hp = hp_status
 
-        hp = "".join([c for c in hp if c in "0123456789/"]).split("/")
-        self._current_hp = int(hp[0])
-        self._max_hp = int(hp[1])
+        current_hp, max_hp = "".join([c for c in hp if c in "0123456789/"]).split("/")
+        self._current_hp = int(current_hp)
+        self._max_hp = int(max_hp)
 
         if store:
             self._stats["hp"] = self._max_hp
@@ -822,6 +821,7 @@ def pokeball(self) -> Optional[str]:
         """
         if self._last_request is not None:
             return self._last_request.get("pokeball", None)
+        return None
 
     @property
     def possible_abilities(self) -> List[str]:
@@ -915,7 +915,7 @@ def status_counter(self) -> int:
         """
         return self._status_counter
 
-    @status.setter
+    @status.setter  # type: ignore
     def status(self, status: Optional[Union[Status, str]]):
         self._status = Status[status.upper()] if isinstance(status, str) else status
 
diff --git a/src/poke_env/player/baselines.py b/src/poke_env/player/baselines.py
index 7e74649a2..8729e0710 100644
--- a/src/poke_env/player/baselines.py
+++ b/src/poke_env/player/baselines.py
@@ -29,7 +29,7 @@ def choose_singles_move(self, battle: AbstractBattle):
         return self.choose_random_move(battle)
 
     def choose_doubles_move(self, battle: DoubleBattle):
-        orders = []
+        orders: List[BattleOrder] = []
         switched_in = None
 
         if any(battle.force_switch):
diff --git a/src/poke_env/player/env_player.py b/src/poke_env/player/env_player.py
index 405b144ce..7f32b659e 100644
--- a/src/poke_env/player/env_player.py
+++ b/src/poke_env/player/env_player.py
@@ -161,7 +161,7 @@ def reward_computing_helper(
         """
         if battle not in self._reward_buffer:
             self._reward_buffer[battle] = starting_value
-        current_value = 0
+        current_value = 0.0
 
         for mon in battle.team.values():
             current_value += mon.current_hp_fraction * hp_value
diff --git a/src/poke_env/player/openai_api.py b/src/poke_env/player/openai_api.py
index 83ddd78bb..563d7677d 100644
--- a/src/poke_env/player/openai_api.py
+++ b/src/poke_env/player/openai_api.py
@@ -94,22 +94,9 @@ def _battle_finished_callback(self, battle: AbstractBattle):
         asyncio.run_coroutine_threadsafe(self.observations.async_put(to_put), POKE_LOOP)
 
 
-class _ABCMetaclass(type(ABC)):
-    pass
-
-
-class _EnvMetaclass(type(Env)):
-    pass
-
-
-class _OpenAIGymEnvMetaclass(_EnvMetaclass, _ABCMetaclass):
-    pass
-
-
 class OpenAIGymEnv(
     Env[ObsType, ActType],
     ABC,
-    metaclass=_OpenAIGymEnvMetaclass,
 ):
     """
     Base class implementing the OpenAI Gym API on the main thread.
@@ -298,7 +285,7 @@ def _get_opponent(self) -> Union[Player, str]:
         random_opponent = (
             random.choice(opponent) if isinstance(opponent, list) else opponent
         )
-        return random_opponent
+        return random_opponent  # type: ignore
 
     def reset(
         self,
diff --git a/src/poke_env/player/player.py b/src/poke_env/player/player.py
index 330a28a65..dded915dd 100644
--- a/src/poke_env/player/player.py
+++ b/src/poke_env/player/player.py
@@ -144,13 +144,12 @@ def __init__(
         )
         self._battle_end_condition: Condition = create_in_poke_loop(Condition)
         self._challenge_queue: Queue[Any] = create_in_poke_loop(Queue)
+        self._team: Optional[Teambuilder] = None
 
         if isinstance(team, Teambuilder):
             self._team = team
         elif isinstance(team, str):
             self._team = ConstantTeambuilder(team)
-        else:
-            self._team = None
 
         self.logger.debug("Player initialisation finished")
 
@@ -197,7 +196,7 @@ async def _create_battle(self, split_message: List[str]) -> AbstractBattle:
             else:
                 gen = GenData.from_format(self._format).gen
                 if self.format_is_doubles:
-                    battle = DoubleBattle(
+                    battle: AbstractBattle = DoubleBattle(
                         battle_tag=battle_tag,
                         username=self.username,
                         logger=self.logger,
@@ -383,10 +382,10 @@ async def _handle_battle_request(
                 return
             message = self.teampreview(battle)
         else:
-            message = self.choose_move(battle)
-            if isinstance(message, Awaitable):
-                message = await message
-            message = message.message
+            choice = self.choose_move(battle)
+            if isinstance(choice, Awaitable):
+                choice = await choice
+            message = choice.message
 
         await self.ps_client.send_message(message, battle.battle_tag)
 
@@ -659,7 +658,7 @@ def choose_random_move(self, battle: AbstractBattle) -> BattleOrder:
             return self.choose_random_singles_move(battle)
         else:
             raise ValueError(
-                "battle should be Battle or DoubleBattle. Received %d" % (type(battle))
+                f"battle should be Battle or DoubleBattle. Received {type(battle)}"
             )
 
     async def ladder(self, n_games: int):
diff --git a/src/poke_env/player/utils.py b/src/poke_env/player/utils.py
index 73d0f1a97..e2d9dc24d 100644
--- a/src/poke_env/player/utils.py
+++ b/src/poke_env/player/utils.py
@@ -167,7 +167,7 @@ async def evaluate_player(
     "battles to perform to evaluate the player."
 
     # Initial placement battles
-    baselines = [p(max_concurrent_battles=n_battles) for p in _EVALUATION_RATINGS]
+    baselines = [p(max_concurrent_battles=n_battles) for p in _EVALUATION_RATINGS]  # type: ignore
 
     for p in baselines:
         await p.battle_against(player, n_placement_battles)
diff --git a/src/poke_env/ps_client/ps_client.py b/src/poke_env/ps_client/ps_client.py
index 488e0ef1e..601592877 100644
--- a/src/poke_env/ps_client/ps_client.py
+++ b/src/poke_env/ps_client/ps_client.py
@@ -11,6 +11,7 @@
 
 import requests
 import websockets.client as ws
+from websockets.client import WebSocketClientProtocol
 from websockets.exceptions import ConnectionClosedOK
 
 from poke_env.concurrency import (
@@ -76,7 +77,7 @@ def __init__(
         self._logged_in: Event = create_in_poke_loop(Event)
         self._sending_lock = create_in_poke_loop(Lock)
 
-        self.websocket: ws.WebSocketClientProtocol
+        self.websocket: WebSocketClientProtocol
         self._logger: Logger = self._create_logger(log_level)
 
         if start_listening:
diff --git a/src/poke_env/teambuilder/teambuilder.py b/src/poke_env/teambuilder/teambuilder.py
index 76777eb05..994037d77 100644
--- a/src/poke_env/teambuilder/teambuilder.py
+++ b/src/poke_env/teambuilder/teambuilder.py
@@ -62,20 +62,16 @@ def parse_showdown_team(team: str) -> List[TeambuilderPokemon]:
                 current_mon.happiness = int(happiness.strip())
             elif line.startswith("EVs: "):
                 evs = line.replace("EVs: ", "")
-                evs = evs.split(" / ")
-                for ev in evs:
-                    ev = ev.split(" ")
-                    n = ev[0]
-                    stat = ev[1]
+                split_evs = evs.split(" / ")
+                for ev in split_evs:
+                    n, stat = ev.split(" ")[:2]
                     idx = STATS_TO_IDX[stat.lower()]
                     current_mon.evs[idx] = int(n)
             elif line.startswith("IVs: "):
                 ivs = line.replace("IVs: ", "")
-                ivs = ivs.split(" / ")
-                for iv in ivs:
-                    iv = iv.split(" ")
-                    n = iv[0]
-                    stat = iv[1]
+                ivs_split = ivs.split(" / ")
+                for iv in ivs_split:
+                    n, stat = iv.split(" ")[:2]
                     idx = STATS_TO_IDX[stat.lower()]
                     current_mon.ivs[idx] = int(n)
             elif line.startswith("- "):


Test patch:
diff --git a/unit_tests/environment/test_battle.py b/unit_tests/environment/test_battle.py
index 806b2e1d7..573307d99 100644
--- a/unit_tests/environment/test_battle.py
+++ b/unit_tests/environment/test_battle.py
@@ -329,6 +329,11 @@ def test_battle_request_and_interactions(example_request):
     battle.end_turn(1)
     assert Effect.RAGE_POWDER not in battle.opponent_active_pokemon.effects
 
+    battle.parse_message(
+        ["", "-singlemove", "p1: Tyranitar", "Glaive Rush", "[silent]"]
+    )
+    assert Effect.GLAIVE_RUSH in battle.opponent_active_pokemon.effects
+
     battle.parse_message(
         [
             "",