Repository: hsahovic/poke-env

Issue:
Hospitality not supported
Hospitality is accidentally assigned to the pokemon receiving the healing of hospitality because of weird showdown protocol

Discussion:

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/fixture_data/example_doubles_request.json b/fixture_data/example_doubles_request.json
index 463412616..62c68a26e 100644
--- a/fixture_data/example_doubles_request.json
+++ b/fixture_data/example_doubles_request.json
@@ -102,7 +102,7 @@
                 "baseAbility": "screencleaner",
                 "condition": "250/250",
                 "details": "Mr. Rime, L77, M",
-                "ident": "p1: Mr. Rime",
+                "ident": "p1: Nickname",
                 "item": "sitrusberry",
                 "moves": ["psychic", "rapidspin", "freezedry", "slackoff"],
                 "pokeball": "pokeball",
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..99972dc9e 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",
@@ -84,6 +83,7 @@ class AbstractBattle(ABC):
         "_format",
         "_gen",
         "in_team_preview",
+        "_last_request",
         "_max_team_size",
         "_maybe_trapped",
         "_move_on_next_request",
@@ -103,7 +103,6 @@ class AbstractBattle(ABC):
         "_rating",
         "_reconnected",
         "_replay_data",
-        "_rqid",
         "rules",
         "_reviving",
         "_save_replays",
@@ -159,10 +158,13 @@ def __init__(
         # Battle state attributes
         self._dynamax_turn: Optional[int] = None
         self._finished: bool = False
-        self._rqid = 0
+        self._last_request: Dict[str, Any] = {}
         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
@@ -219,12 +221,13 @@ def get_pokemon(
             return self._opponent_team[identifier]
 
         player_role = identifier[:2]
+        name = identifier[3:].strip()
         is_mine = player_role == self._player_role
 
         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(
@@ -238,12 +241,14 @@ def get_pokemon(
             )
 
         if request:
-            team[identifier] = Pokemon(request_pokemon=request, gen=self._data.gen)
+            team[identifier] = Pokemon(
+                request_pokemon=request, name=name, gen=self._data.gen
+            )
         elif details:
-            team[identifier] = Pokemon(details=details, gen=self._data.gen)
+            team[identifier] = Pokemon(details=details, name=name, gen=self._data.gen)
         else:
             species = identifier[4:]
-            team[identifier] = Pokemon(species=species, gen=self._data.gen)
+            team[identifier] = Pokemon(species=species, name=name, gen=self._data.gen)
 
         return team[identifier]
 
@@ -308,10 +313,15 @@ def _check_heal_message_for_ability(self, split_message: List[str]):
         #   it implies the ability is from the opposite side
         # Example:
         #   |-heal|p2a: Quagsire|100/100|[from] ability: Water Absorb|[of] p1a: Genesect
+        #   |-heal|p2b: Excadrill|100/100|from] ability: Hospitality|[of] p2a: Sinistcha
         if len(split_message) == 6 and split_message[4].startswith("[from] ability:"):
-            ability = split_message[4].split("ability:")[-1]
-            pkmn = split_message[2]
-            self.get_pokemon(pkmn).ability = to_id_str(ability)
+            ability = to_id_str(split_message[4].split("ability:")[-1])
+            if ability == "hospitality":
+                pkmn = split_message[5].replace("[of] ", "").strip()
+                self.get_pokemon(pkmn).ability = ability
+            else:
+                pkmn = split_message[2]
+                self.get_pokemon(pkmn).ability = ability
 
     @abstractmethod
     def end_illusion(self, pokemon_name: str, details: str):
@@ -330,8 +340,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}")
 
@@ -443,6 +453,8 @@ 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 "
@@ -598,10 +610,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 +629,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 +694,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 +721,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 +797,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 +821,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 +842,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:
@@ -951,6 +962,16 @@ 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]:
         """
@@ -961,6 +982,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]:
@@ -1000,6 +1022,17 @@ def gen(self) -> int:
         """
         return self._gen
 
+    @property
+    def last_request(self) -> Dict[str, Any]:
+        """
+        The last request received from the server. This allows players to track
+            rqid and also maintain parallel battle copies for search/inference
+
+        :return: The last request.
+        :rtype: Dict[str, Any]
+        """
+        return self._last_request
+
     @property
     def lost(self) -> Optional[bool]:
         """
@@ -1059,6 +1092,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 +1104,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]:
@@ -1176,16 +1211,6 @@ def opponent_rating(self) -> Optional[int]:
         """
         return self._opponent_rating
 
-    @property
-    def rqid(self) -> int:
-        """
-        Should not be used.
-
-        :return: The last request's rqid.
-        :rtype: int
-        """
-        return self._rqid
-
     @property
     def side_conditions(self) -> Dict[SideCondition, int]:
         """
diff --git a/src/poke_env/environment/battle.py b/src/poke_env/environment/battle.py
index f0cdc92de..655baf728 100644
--- a/src/poke_env/environment/battle.py
+++ b/src/poke_env/environment/battle.py
@@ -84,8 +84,7 @@ def parse_request(self, request: Dict[str, Any]) -> None:
         if self._force_switch:
             self._move_on_next_request = True
 
-        if request["rqid"]:
-            self._rqid = max(self._rqid, request["rqid"])
+        self._last_request = request
 
         if request.get("teamPreview", False):
             self._teampreview = True
@@ -159,6 +158,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 +258,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 +278,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/double_battle.py b/src/poke_env/environment/double_battle.py
index 644cef47f..07f1e2f4e 100644
--- a/src/poke_env/environment/double_battle.py
+++ b/src/poke_env/environment/double_battle.py
@@ -117,8 +117,7 @@ def parse_request(self, request: Dict[str, Any]) -> None:
         if any(self._force_switch):
             self._move_on_next_request = True
 
-        if request["rqid"]:
-            self._rqid = max(self._rqid, request["rqid"])
+        self._last_request = request
 
         if request.get("teamPreview", False):
             self._teampreview = True
@@ -294,6 +293,8 @@ def get_possible_showdown_targets(
             PokemonType.GHOST not in pokemon.types
         ):  # fixing target for Curse
             return [self.EMPTY_TARGET_POSITION]
+        elif move.id == "terastarstorm" and pokemon.type_1 == PokemonType.STELLAR:
+            targets = [self.EMPTY_TARGET_POSITION]
         else:
             targets = {
                 Target.from_showdown_message("adjacentAlly"): [ally_position],
diff --git a/src/poke_env/environment/effect.py b/src/poke_env/environment/effect.py
index 0fe111a39..16a3e477d 100644
--- a/src/poke_env/environment/effect.py
+++ b/src/poke_env/environment/effect.py
@@ -30,6 +30,7 @@ class Effect(Enum):
     CELEBRATE = auto()
     CHARGE = auto()
     CLAMP = auto()
+    COMMANDER = auto()
     CONFUSION = auto()
     COURT_CHANGE = auto()
     CRAFTY_SHIELD = auto()
@@ -97,6 +98,7 @@ class Effect(Enum):
     INGRAIN = auto()
     INNARDS_OUT = auto()
     INSOMNIA = auto()
+    INSTRUCT = auto()
     IRON_BARBS = auto()
     KINGS_SHIELD = auto()
     LASER_FOCUS = auto()
@@ -219,7 +221,7 @@ class Effect(Enum):
     TRAPPED = auto()
     TRICK = auto()
     TYPEADD = auto()
-    TYPE_CHANGE = auto()
+    TYPECHANGE = auto()
     UPROAR = auto()
     VITAL_SPIRIT = auto()
     WANDERING_SPIRIT = auto()
@@ -299,6 +301,14 @@ def breaks_protect(self):
         """
         return self in _PROTECT_BREAKING_EFFECTS
 
+    @property
+    def ends_on_move(self) -> bool:
+        """
+        :return: Whether this effect ends when a pokemon moves.
+        :rtype: bool
+        """
+        return self in _ENDS_ON_MOVE_EFFECTS
+
     @property
     def ends_on_switch(self) -> bool:
         """
@@ -371,6 +381,7 @@ def is_from_move(self) -> bool:
     Effect.AROMA_VEIL,
     Effect.BAD_DREAMS,
     Effect.BATTLE_BOND,
+    Effect.COMMANDER,
     Effect.CUD_CHEW,
     Effect.DANCER,
     Effect.DISGUISE,
@@ -512,6 +523,7 @@ def is_from_move(self) -> bool:
     Effect.INFESTATION,
     Effect.INGRAIN,
     Effect.INSOMNIA,
+    Effect.INSTRUCT,
     Effect.KINGS_SHIELD,
     Effect.LASER_FOCUS,
     Effect.LEECH_SEED,
@@ -587,7 +599,7 @@ def is_from_move(self) -> bool:
     Effect.TRAPPED,
     Effect.TRICK,
     Effect.TYPEADD,
-    Effect.TYPE_CHANGE,
+    Effect.TYPECHANGE,
     Effect.UPROAR,
     Effect.WHIRLPOOL,
     Effect.WIDE_GUARD,
@@ -707,12 +719,23 @@ def is_from_move(self) -> bool:
 _ENDS_ON_MOVE_EFFECTS = {
     Effect.GLAIVE_RUSH,
     Effect.CHARGE,
+    Effect.DANCER,
+    Effect.GRUDGE,
+    Effect.DESTINY_BOND,
+    Effect.RAGE,
+    Effect.INSTRUCT,
 }
 
 _ENDS_ON_SWITCH_EFFECTS = {
     Effect.MIND_READER,
     Effect.MUMMY,
     Effect.SPEED_SWAP,
+    Effect.TYPECHANGE,
+    Effect.SKILL_SWAP,
+    Effect.PERISH0,
+    Effect.PERISH1,
+    Effect.PERISH2,
+    Effect.PERISH3,
 }
 
 _ENDS_ON_TURN_EFFECTS = {
@@ -738,6 +761,7 @@ def is_from_move(self) -> bool:
     Effect.SPIKY_SHIELD,
     Effect.SPOTLIGHT,
     Effect.WIDE_GUARD,
+    Effect.INSTRUCT,
 }
 
 _ACTION_COUNTER_EFFECTS: Set[Effect] = {Effect.RAGE, Effect.STOCKPILE}
@@ -761,6 +785,7 @@ def is_from_move(self) -> bool:
     "CELEBRATE": Effect.CELEBRATE,
     "CHARGE": Effect.CHARGE,
     "CLAMP": Effect.CLAMP,
+    "COMMANDER": Effect.COMMANDER,
     "CONFUSION": Effect.CONFUSION,
     "COURTCHANGE": Effect.COURT_CHANGE,
     "CRAFTYSHIELD": Effect.CRAFTY_SHIELD,
@@ -827,6 +852,7 @@ def is_from_move(self) -> bool:
     "INFESTATION": Effect.INFESTATION,
     "INGRAIN": Effect.INGRAIN,
     "INNARDSOUT": Effect.INNARDS_OUT,
+    "INSTRUCT": Effect.INSTRUCT,
     "INSOMNIA": Effect.INSOMNIA,
     "IRONBARBS": Effect.IRON_BARBS,
     "KINGSSHIELD": Effect.KINGS_SHIELD,
@@ -950,7 +976,7 @@ def is_from_move(self) -> bool:
     "TRAPPED": Effect.TRAPPED,
     "TRICK": Effect.TRICK,
     "TYPEADD": Effect.TYPEADD,
-    "TYPECHANGE": Effect.TYPE_CHANGE,
+    "TYPECHANGE": Effect.TYPECHANGE,
     "UPROAR": Effect.UPROAR,
     "VITALSPIRIT": Effect.VITAL_SPIRIT,
     "WANDERINGSPIRIT": Effect.WANDERING_SPIRIT,
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..0cab4d304 100644
--- a/src/poke_env/environment/pokemon.py
+++ b/src/poke_env/environment/pokemon.py
@@ -33,6 +33,7 @@ class Pokemon:
         "_max_hp",
         "_moves",
         "_must_recharge",
+        "_name",
         "_possible_abilities",
         "_preparing_move",
         "_preparing_target",
@@ -55,6 +56,7 @@ def __init__(
         gen: int,
         *,
         species: Optional[str] = None,
+        name: Optional[str] = None,
         request_pokemon: Optional[Dict[str, Any]] = None,
         details: Optional[str] = None,
         teambuilder: Optional[TeambuilderPokemon] = None,
@@ -73,11 +75,11 @@ 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
         self._moves: Dict[str, Move] = {}
+        self._name: Optional[str] = None
         self._shiny: Optional[bool] = False
 
         # Battle related attributes
@@ -102,7 +104,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]] = {
@@ -125,6 +127,9 @@ def __init__(
         elif teambuilder:
             self._update_from_teambuilder(teambuilder)
 
+        if name is not None:
+            self._name = name
+
     def __repr__(self) -> str:
         return self.__str__()
 
@@ -144,7 +149,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 +278,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 +339,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
@@ -479,6 +484,7 @@ def update_from_request(self, request_pokemon: Dict[str, Any]):
         condition = request_pokemon["condition"]
         self.set_hp_status(condition, store=True)
 
+        self._name = request_pokemon["ident"][4:]
         self._item = request_pokemon["item"]
 
         details = request_pokemon["details"]
@@ -502,10 +508,16 @@ def update_from_request(self, request_pokemon: Dict[str, Any]):
                 self._stats[stat] = request_pokemon["stats"][stat]
 
     def _update_from_teambuilder(self, tb: TeambuilderPokemon):
-        if tb.nickname and not tb.species:
+        if tb.nickname is not None and tb.species is None:
             self._update_from_pokedex(tb.nickname)
-        elif tb.nickname and tb.species:
+        elif tb.nickname is not None and tb.species is not None:
             self._update_from_pokedex(tb.species)
+            self._name = tb.nickname
+        else:
+            raise ValueError(
+                "TeambuilderPokemon must have either a nickname or species", tb
+            )
+
         if tb.level:
             self._level = tb.level
         self._ability = to_id_str(tb.ability)
@@ -520,10 +532,11 @@ def _update_from_teambuilder(self, tb: TeambuilderPokemon):
             move = Move(Move.retrieve_id(move_str), gen=self._data.gen)
             self._moves[move.id] = move
 
-        if tb.level and tb.nature:
+        if tb.level:
+            nature = tb.nature.lower() if tb.nature else "serious"
             self._stats = {}
             stats = compute_raw_stats(
-                self._species, tb.evs, tb.ivs, tb.level, tb.nature.lower(), self._data
+                self._species, tb.evs, tb.ivs, tb.level, nature, self._data
             )
             for stat, val in zip(["hp", "atk", "def", "spa", "spd", "spe"], stats):
                 self._stats[stat] = val
@@ -654,9 +667,7 @@ def base_species(self) -> str:
         :rtype: str
         """
         dex_entry = self._data.pokedex[self._species]
-        if "baseSpecies" in dex_entry:
-            return to_id_str(dex_entry["baseSpecies"])
-        return self._species
+        return to_id_str(dex_entry["baseSpecies"])
 
     @property
     def base_stats(self) -> Dict[str, int]:
@@ -808,11 +819,18 @@ def must_recharge(self, value: bool):
     @property
     def name(self) -> str:
         """
-        :return: A string of the pokemon's name, as it appears in Showdown
-        :rtype: name
+        :return: The pokemon's name, which can be used to create Showdown's
+            identifier.
+        :rtype: str
         """
-        dex_entry = self._data.pokedex[self._species]
-        return dex_entry["name"]
+        if self._name is not None:
+            return self._name
+        else:
+            dex_entry = self._data.pokedex[self._species]
+            if dex_entry["baseSpecies"].islower():
+                return dex_entry["name"]
+            else:
+                return dex_entry["baseSpecies"]
 
     @property
     def pokeball(self) -> Optional[str]:
@@ -822,6 +840,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 +934,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(
         [
             "",
diff --git a/unit_tests/environment/test_double_battle.py b/unit_tests/environment/test_double_battle.py
index 9b9372980..c4711c231 100644
--- a/unit_tests/environment/test_double_battle.py
+++ b/unit_tests/environment/test_double_battle.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from poke_env.environment import DoubleBattle, Pokemon
+from poke_env.environment import DoubleBattle, Move, Pokemon
 
 
 def test_battle_request_parsing(example_doubles_request):
@@ -45,7 +45,7 @@ def test_battle_request_parsing_and_interactions(example_doubles_request):
     assert their_first_active is None and their_second_active is None
     assert isinstance(mr_rime, Pokemon)
     assert isinstance(klinklang, Pokemon)
-    assert battle.get_pokemon("p1: Mr. Rime") == mr_rime
+    assert battle.get_pokemon("p1: Nickname") == mr_rime
     assert battle.get_pokemon("p1: Klinklang") == klinklang
 
     assert set(battle.available_moves[0]) == set(
@@ -110,6 +110,43 @@ def test_battle_request_parsing_and_interactions(example_doubles_request):
 
     assert all(battle.opponent_can_dynamax)
 
+    assert battle.current_observation
+    assert battle.current_observation.events[0] == ["", "swap", "p1b: Klinklang", ""]
+
+
+def test_check_heal_message_for_ability():
+    logger = MagicMock()
+    battle = DoubleBattle("tag", "username", logger, gen=8)
+    battle.player_role = "p1"
+
+    # Add two active opponent pokemon
+    battle.parse_message(["", "switch", "p2a: Furret", "Furret, L50, F", "100/100"])
+    battle.parse_message(["", "switch", "p2b: Sentret", "Sentret, L50, F", "100/100"])
+
+    battle.parse_message(
+        [
+            "",
+            "-heal",
+            "p2a: Furret",
+            "100/100",
+            "[from] ability: Water Absorb",
+            "[of] p2b: Sentret",
+        ]
+    )
+    assert battle.opponent_team["p2: Furret"].ability == "waterabsorb"
+
+    battle.parse_message(
+        [
+            "",
+            "-heal",
+            "p2b: Furret",
+            "100/100",
+            "[from] ability: Hospitality",
+            "[of] p2a: Sentret",
+        ]
+    )
+    assert battle.opponent_team["p2: Sentret"].ability == "hospitality"
+
 
 def test_get_possible_showdown_targets(example_doubles_request):
     logger = MagicMock()
@@ -132,6 +169,13 @@ def test_get_possible_showdown_targets(example_doubles_request):
     ]
     assert battle.get_possible_showdown_targets(slackoff, mr_rime, dynamax=True) == [0]
 
+    # Override last request with terastarstorm for Mr. Rime
+    terastarstorm = Move("terastarstorm", gen=9)
+    battle._available_moves = [[terastarstorm], []]
+    assert battle.get_possible_showdown_targets(terastarstorm, mr_rime) == [-2, 1, 2]
+    mr_rime.terastallize("stellar")
+    assert battle.get_possible_showdown_targets(terastarstorm, mr_rime) == [0]
+
 
 def test_to_showdown_target(example_doubles_request):
     logger = MagicMock()
@@ -270,6 +314,8 @@ def test_one_mon_left_in_double_battles_results_in_available_move_in_the_correct
     battle.parse_message(["", "turn", "1"])
 
     battle.parse_request(request)
+    assert battle.last_request == request
+
     battle.parse_message(
         ["", "swap", "p1b: Cresselia", "0", "[from] move: Ally Switch"]
     )
@@ -307,3 +353,35 @@ def test_gen_and_format(example_doubles_logs):
     assert battle.gen == 6
     assert battle.battle_tag == "tag"
     assert battle.format == "gen6doublesou"
+
+
+def test_pledge_moves():
+    battle = DoubleBattle("tag", "username", MagicMock(), gen=8)
+    battle.player_role = "p2"
+
+    events = [
+        ["", "switch", "p1a: Indeedee", "Indeedee-F, L50, F", "100/100"],
+        ["", "switch", "p1b: Hatterene", "Hatterene, L50, F", "100/100"],
+        ["", "switch", "p2a: Primarina", "Primarina, L50, F, shiny", "169/169"],
+        ["", "switch", "p2b: Decidueye", "Decidueye-Hisui, L50, F, shiny", "171/171"],
+        ["", ""],
+        ["", "move", "p2b: Decidueye", "Grass Pledge", "p1a: Indeedee"],
+        ["", "-waiting", "p2b: Decidueye", "p2a: Primarina"],
+        [
+            "",
+            "move",
+            "p2a: Primarina",
+            "Water Pledge",
+            "p1b: Hatterene",
+            "[from]move: Grass Pledge",
+        ],
+        ["", "-combine"],
+        ["", "-damage", "p1b: Hatterene", "0 fnt"],
+        ["", "-sidestart", "p1: cloverspsyspamsep", "Grass Pledge"],
+    ]
+
+    for event in events:
+        battle.parse_message(event)
+
+    assert "grasspledge" not in battle.team["p2: Primarina"].moves
+    assert "waterpledge" in battle.team["p2: Primarina"].moves
diff --git a/unit_tests/environment/test_enumerations.py b/unit_tests/environment/test_enumerations.py
index 8c4531cf6..6fd688dac 100644
--- a/unit_tests/environment/test_enumerations.py
+++ b/unit_tests/environment/test_enumerations.py
@@ -55,6 +55,9 @@ def test_effect_build():
     assert Effect["OCTOLOCK"].is_from_move
     assert not Effect["TERA_SHELL"].is_from_move
 
+    assert Effect["DESTINY_BOND"].ends_on_move
+    assert not Effect["HEAL_BLOCK"].ends_on_move
+
     assert Effect.from_data("i dont know") == Effect.UNKNOWN
 
     # By going through all moves, we know we have an Effect for every Volatile Status
diff --git a/unit_tests/environment/test_pokemon.py b/unit_tests/environment/test_pokemon.py
index e73d99fdc..4bc9aacdb 100644
--- a/unit_tests/environment/test_pokemon.py
+++ b/unit_tests/environment/test_pokemon.py
@@ -1,4 +1,6 @@
-from poke_env.environment import Move, Pokemon, PokemonGender, PokemonType
+from unittest.mock import MagicMock
+
+from poke_env.environment import DoubleBattle, Move, Pokemon, PokemonGender, PokemonType
 from poke_env.stats import _raw_hp, _raw_stat
 from poke_env.teambuilder.teambuilder import Teambuilder
 
@@ -214,21 +216,60 @@ def test_teambuilder(showdown_format_teams):
     )
     mon = Pokemon(9, teambuilder=tb_mons[0])
     assert mon
-    assert mon.name == "Weezing-Galar"
+    assert mon.name == "Weezing"
     assert mon.level == 50
     assert mon.ability == "neutralizinggas"
     assert mon.item is None
     assert list(mon.moves.keys()) == ["clearsmog"]
     assert mon.tera_type is None
+    assert mon.stats == {
+        "hp": 140,
+        "atk": 110,
+        "def": 140,
+        "spa": 105,
+        "spd": 90,
+        "spe": 80,
+    }
 
 
-def test_name():
+def test_name(example_doubles_request):
     charizard = Pokemon(species="charizard", gen=8)
     assert charizard.name == "Charizard"
 
     chiyu = Pokemon(species="chiyu", gen=9)
     assert chiyu.name == "Chi-Yu"
 
+    furret = Pokemon(species="furret", name="Nickname", gen=9)
+    assert furret.name == "Nickname"
+
+    calyrexshadow = Pokemon(species="calyrexshadow", gen=9)
+    assert calyrexshadow.name == "Calyrex"
+
+    zamazenta = Pokemon(species="zamazentacrowned", gen=9)
+    assert zamazenta.name == "Zamazenta"
+
+    logger = MagicMock()
+    battle = DoubleBattle("tag", "username", logger, gen=8)
+    battle.parse_request(example_doubles_request)
+
+    mon = battle.team["p1: Nickname"]
+    assert mon.name == "Nickname"
+    assert mon.species == "mrrime"
+
+    tb_mons = Teambuilder.parse_showdown_team(
+        "Weezing-Galar\nAbility: Neutralizing Gas\nLevel: 50\n- Clear Smog"
+    )
+    mon = Pokemon(9, teambuilder=tb_mons[0])
+    assert mon.species == "weezinggalar"
+    assert mon.name == "Weezing"
+
+    tb_mons = Teambuilder.parse_showdown_team(
+        "Nickname (Zamazenta-Crowned)\nAbility: Dauntless Shield\nLevel: 50\n- Behemoth Bash"
+    )
+    mon = Pokemon(9, teambuilder=tb_mons[0])
+    assert mon.species == "zamazentacrowned"
+    assert mon.name == "Nickname"
+
 
 def test_stats(example_request, showdown_format_teams):
     request_mons = example_request["side"]["pokemon"]