diff --git a/src/poke_env/battle/abstract_battle.py b/src/poke_env/battle/abstract_battle.py index 6e4195a32..3337ca978 100644 --- a/src/poke_env/battle/abstract_battle.py +++ b/src/poke_env/battle/abstract_battle.py @@ -6,6 +6,7 @@ from poke_env.battle.effect import Effect from poke_env.battle.field import Field +from poke_env.battle.move import Move from poke_env.battle.observation import Observation from poke_env.battle.observed_pokemon import ObservedPokemon from poke_env.battle.pokemon import Pokemon @@ -473,9 +474,10 @@ def parse_message(self, split_message: List[str]): self._check_damage_message_for_item(event) self._check_damage_message_for_ability(event) elif event[1] == "move": + use = True failed = False - override_move = None - reveal_other_move = False + reveal = True + overridden_move = None for move_failed_suffix in ["[miss]", "[still]", "[notarget]"]: if event[-1] == move_failed_suffix: @@ -485,37 +487,47 @@ def parse_message(self, split_message: List[str]): if event[-1] == "[notarget]": event = event[:-1] - if event[-1].startswith("[spread]"): + while event[-1].startswith("[spread]"): event = event[:-1] if event[-1] in { "[from] lockedmove", - "[from] Pursuit", "[from]lockedmove", "[from] Sky Attack", - "[from]Pursuit", - "[zeffect]", }: + use = False + reveal = False event = event[:-1] + if event[-1] in {"[from] Pursuit", "[from]Pursuit", "[zeffect]"}: + event = event[:-1] + + if event[-1] == "[from] Sleep Talk": + event[-1] = "[from] move: Sleep Talk" + if event[-1].startswith("[anim]"): event = event[:-1] if event[-1].startswith(("[from] move: ", "[from]move: ")): - override_move = event.pop().split(": ")[-1] + overridden_move = event.pop().split(": ")[-1] - if override_move == "Sleep Talk": - # Sleep talk was used, but also reveals another move - reveal_other_move = True - elif override_move in {"Copycat", "Metronome", "Nature Power", "Round"}: + if overridden_move == "Sleep Talk": pass - elif override_move in {"Grass Pledge", "Water Pledge", "Fire Pledge"}: - override_move = None + elif overridden_move in { + "Copycat", + "Metronome", + "Nature Power", + "Round", + }: + # triggers moves not owned by actor, so no reveal + reveal = False + elif overridden_move in {"Grass Pledge", "Water Pledge", "Fire Pledge"}: + overridden_move = None elif self.logger is not None: self.logger.warning( "Unmanaged [from] move message received - move %s in cleaned up " "message %s in battle %s turn %d", - override_move, + overridden_move, event, self.battle_tag, self.turn, @@ -531,7 +543,8 @@ def parse_message(self, split_message: List[str]): self.get_pokemon(pokemon).ability = revealed_ability if revealed_ability == "Magic Bounce": - return + use = False + reveal = False elif revealed_ability == "Dancer": return elif self.logger is not None: @@ -543,21 +556,22 @@ def parse_message(self, split_message: List[str]): self.battle_tag, self.turn, ) - if event[-1] == "[from] Magic Coat": - return - - while event[-1] == "[still]": + if event[-1] == "[from] Magic Coat" or event[-1] == "[from] Mirror Move": + use = False + reveal = False event = event[:-1] - if event[-1] == "": + while event[-1] == "[still]": event = event[:-1] + presumed_target = None if len(event) == 4: pokemon, move = event[2:4] elif len(event) == 5: pokemon, move, presumed_target = event[2:5] - - if len(presumed_target) > 4 and presumed_target[:4] in { + if presumed_target == "": + pass + elif len(presumed_target) > 4 and presumed_target[:4] in { "p1: ", "p2: ", "p1a:", @@ -594,16 +608,31 @@ def parse_message(self, split_message: List[str]): temp_pokemon = self.get_pokemon(pokemon) temp_pokemon.start_effect("MINIMIZE") - if override_move: - # Moves that can trigger this branch results in two `move` messages being sent. - # We're setting use=False in the one (with the override) in order to prevent two pps from being used - # incorrectly. - self.get_pokemon(pokemon).moved(override_move, failed=failed, use=False) - if override_move is None or reveal_other_move: - self.get_pokemon(pokemon).moved(move, failed=failed, use=False) + pressure = self._pressure_on(pokemon, move, presumed_target or None) + mon = self.get_pokemon(pokemon) + if overridden_move: + mon.moved(move, failed=failed, use=False, reveal=reveal) + overridden = mon.moves[Move.retrieve_id(overridden_move)] + overridden.use(pressure, overridden=True) + elif not failed and move in { + "Sleep Talk", + "Copycat", + "Metronome", + "Nature Power", + }: + # make preemptive deduction in case override move fails + mon.moved(move, failed=failed, use=use, reveal=reveal) + else: + mon.moved( + move, failed=failed, use=use, reveal=reveal, pressure=pressure + ) elif event[1] == "cant": - pokemon, _ = event[2:4] - self.get_pokemon(pokemon).cant_move() + if len(event) == 4: + pokemon, _ = event[2:4] + self.get_pokemon(pokemon).cant_move() + elif len(event) == 5: + pokemon, _, move = event[2:5] + self.get_pokemon(pokemon).cant_move(move) elif event[1] == "turn": # Saving the beginning-of-turn battle state and events as we go into the turn self.observations[self.turn] = self._current_observation @@ -710,6 +739,10 @@ def parse_message(self, split_message: List[str]): types = event[4] mon.start_effect(effect, details=types) else: + if effect == "Mimic": + mon._mimic_move = Move( + Move.retrieve_id(event[4]), gen=self.gen, from_mimic=True + ) mon.start_effect(effect) if mon.is_dynamaxed: @@ -741,6 +774,20 @@ def parse_message(self, split_message: List[str]): "[item] ", "" ) self.get_pokemon(target).item = None + elif effect == "item: Leppa Berry": + mon = self.get_pokemon(target) + mv = mon.moves[to_id_str(event[4])] + # Don't let current pp exceed max pp + mv._current_pp = min(mv._current_pp + 10, mv.max_pp) + elif effect == "move: Mimic": + mon = self.get_pokemon(target) + mon._mimic_move = Move( + Move.retrieve_id(event[4]), gen=self.gen, from_mimic=True + ) + elif effect == "move: Trick": + mon = self.get_pokemon(target) + mon2 = self.get_pokemon(event[4].replace("[of] ", "")) + mon._item, mon2._item = mon2.item, mon.item elif target != "": # ['', '-activate', '', 'move: Splash'] self.get_pokemon(target).start_effect(effect) elif event[1] == "-status": @@ -839,9 +886,11 @@ def parse_message(self, split_message: List[str]): self.get_pokemon(victim).item = None else: raise ValueError(f"Unhandled item message: {event}") - else: pokemon, item = event[2:4] + if len(event) > 4 and event[4] == "[from] move: Trick": + # this event is handled in when consuming -activate event + return self.get_pokemon(pokemon).item = to_id_str(item) elif event[1] == "-mega": assert self.player_role is not None @@ -1034,6 +1083,10 @@ def parse_request( ): pass + @abstractmethod + def _pressure_on(self, pokemon: str, move: str, target_str: Optional[str]) -> bool: + pass + def _register_teampreview_pokemon(self, player: str, details: str): if player != self._player_role: mon = Pokemon(details=details, gen=self.gen) diff --git a/src/poke_env/battle/battle.py b/src/poke_env/battle/battle.py index 201c23530..b3db36864 100644 --- a/src/poke_env/battle/battle.py +++ b/src/poke_env/battle/battle.py @@ -4,6 +4,7 @@ from poke_env.battle.abstract_battle import AbstractBattle from poke_env.battle.move import Move from poke_env.battle.pokemon import Pokemon +from poke_env.data import GenData from poke_env.player.battle_order import DefaultBattleOrder, SingleBattleOrder @@ -96,6 +97,8 @@ def parse_request( self._trapped = True if self.active_pokemon is not None: + if strict_battle_tracking: + self.active_pokemon.check_move_consistency(active_request) # TODO: the illusion handling here works around Zoroark's # difficulties. This should be properly handled at some point. try: @@ -125,6 +128,40 @@ def parse_request( if not pokemon.active and self.reviving == pokemon.fainted: self._available_switches.append(pokemon) + def _pressure_on(self, pokemon: str, move: str, target_str: Optional[str]) -> bool: + move_id = Move.retrieve_id(move) + if move_id not in GenData.from_gen(self.gen).moves: + # This happens when `move` is a z-move. Since z-moves cannot be PP tracked + # anyway, we just return False here. + return False + move_data = GenData.from_gen(self.gen).moves[move_id] + if move_data["target"] == "all" or target_str is None: + target = ( + self.opponent_active_pokemon + if self.player_role == pokemon[:2] + else self.active_pokemon + ) + assert target is not None + else: + target = self.get_pokemon(target_str) + return ( + target.ability == "pressure" + and not target.fainted + and ( + move_data["target"] + in [ + "all", + "allAdjacent", + "allAdjacentFoes", + "any", + "normal", + "randomNormal", + "scripted", + ] + or "mustpressure" in move_data["flags"] + ) + ) + def switch(self, pokemon_str: str, details: str, hp_status: str): identifier = pokemon_str.split(":")[0][:2] diff --git a/src/poke_env/battle/double_battle.py b/src/poke_env/battle/double_battle.py index 4ac79e6d8..455497f93 100644 --- a/src/poke_env/battle/double_battle.py +++ b/src/poke_env/battle/double_battle.py @@ -7,6 +7,7 @@ from poke_env.battle.pokemon import Pokemon from poke_env.battle.pokemon_type import PokemonType from poke_env.battle.target import Target +from poke_env.data import GenData from poke_env.player.battle_order import ( DefaultBattleOrder, PassBattleOrder, @@ -140,6 +141,8 @@ def parse_request( force_self_team=True, details=pokemon_dict["details"], ) + if strict_battle_tracking: + active_pokemon.check_move_consistency(active_request) if self.player_role is not None: if ( active_pokemon_number == 0 @@ -210,6 +213,45 @@ def parse_request( if not pokemon.active and self.reviving == pokemon.fainted: self._available_switches[i].append(pokemon) + def _pressure_on(self, pokemon: str, move: str, target_str: Optional[str]) -> bool: + move_id = Move.retrieve_id(move) + if move_id not in GenData.from_gen(self.gen).moves: + # This happens when `move` is a z-move. Since z-moves cannot be PP tracked + # anyway, we just return False here. + return False + move_data = GenData.from_gen(self.gen).moves[move_id] + if move_data["target"] == "all" or target_str is None: + targets = ( + self.opponent_active_pokemon + if self.player_role == pokemon[:2] + else self.active_pokemon + ) + cleaned_targets = [t for t in targets if t is not None] + if not cleaned_targets: + return False + target = cleaned_targets[0] + for t in cleaned_targets: + if target.ability != "pressure": + target = t + assert target is not None + else: + target = self.get_pokemon(target_str) + return ( + target.ability == "pressure" + and not target.fainted + and move_data["target"] + in [ + "all", + "allAdjacent", + "allAdjacentFoes", + "any", + "normal", + "randomNormal", + "scripted", + ] + or "mustpressure" in move_data["flags"] + ) + def switch(self, pokemon_str: str, details: str, hp_status: str): pokemon_identifier = pokemon_str.split(":")[0][:3] player_identifier = pokemon_identifier[:2] diff --git a/src/poke_env/battle/move.py b/src/poke_env/battle/move.py index 2c4f6a32b..5e4912a0b 100644 --- a/src/poke_env/battle/move.py +++ b/src/poke_env/battle/move.py @@ -12,7 +12,6 @@ from poke_env.data import GenData, to_id_str SPECIAL_MOVES: Set[str] = {"struggle", "recharge"} - _PROTECT_MOVES = { "protect", "detect", @@ -78,14 +77,22 @@ class Move: "_base_power_override", "_current_pp", "_dynamaxed_move", + "_from_mimic", "_gen", "_request_target", ) - def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None): + def __init__( + self, + move_id: str, + gen: int, + raw_id: Optional[str] = None, + from_mimic: bool = False, + ): self._id = move_id self._base_power_override = None self._gen = gen + self._from_mimic = from_mimic if move_id.startswith("hiddenpower") and raw_id is not None: base_power = "".join([c for c in raw_id if c.isdigit()]) @@ -105,8 +112,14 @@ def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None): def __repr__(self) -> str: return f"{self._id} (Move object)" - def use(self): - self._current_pp -= 1 + def use(self, pressure: bool = False, overridden: bool = False): + decrement = 1 + if pressure: + decrement += 1 + if overridden: + decrement -= 1 + # don't let PP go below 0 + self._current_pp = max(self._current_pp - decrement, 0) @staticmethod def is_id_z(id_: str, gen: int) -> bool: @@ -446,7 +459,10 @@ def max_pp(self) -> int: :return: The move's max pp. :rtype: int """ - return self.entry["pp"] * 8 // 5 + max_pp = self.entry["pp"] * 8 // 5 + if self._gen < 3 and not self._from_mimic: + max_pp = min(max_pp, 61) + return max_pp @property def n_hit(self) -> Tuple[int, int]: diff --git a/src/poke_env/battle/pokemon.py b/src/poke_env/battle/pokemon.py index 8637c4bde..bdbb13511 100644 --- a/src/poke_env/battle/pokemon.py +++ b/src/poke_env/battle/pokemon.py @@ -8,6 +8,7 @@ from poke_env.battle.pokemon_gender import PokemonGender from poke_env.battle.pokemon_type import PokemonType from poke_env.battle.status import Status +from poke_env.battle.target import Target from poke_env.battle.z_crystal import Z_CRYSTAL from poke_env.data import GenData, to_id_str from poke_env.stats import compute_raw_stats @@ -32,6 +33,7 @@ class Pokemon: "_last_request", "_level", "_max_hp", + "_mimic_move", "_forme_change_ability", "_moves", "_must_recharge", @@ -47,6 +49,8 @@ class Pokemon: "_status", "_status_counter", "_temporary_ability", + "_temporary_base_stats", + "_transform_moves", "_temporary_types", "_terastallized", "_terastallized_type", @@ -120,7 +124,10 @@ def __init__( self._status_counter: int = 0 self._temporary_ability: Optional[str] = None self._forme_change_ability: Optional[str] = None + self._temporary_base_stats: Optional[Dict[str, int]] = None + self._transform_moves: Optional[Dict[str, Move]] = None self._temporary_types: List[PokemonType] = [] + self._mimic_move: Optional[Move] = None if request_pokemon: self.update_from_request(request_pokemon) @@ -148,20 +155,17 @@ def __str__(self) -> str: f"[Active: {self._active}, Status: {status_repr}]" ) - def _add_move(self, move_id: str, use: bool = False) -> Optional[Move]: + def _add_move(self, move_id: str) -> Optional[Move]: """Store the move if applicable.""" id_ = Move.retrieve_id(move_id) - + if id_ in self.moves: + return self.moves[id_] if not Move.should_be_stored(id_, self.gen): return None - - if id_ not in self._moves: + if id_ not in self.base_moves: move = Move(move_id=id_, raw_id=move_id, gen=self.gen) - self._moves[id_] = move - if use: - self._moves[id_].use() - - return self._moves[id_] + self.base_moves[id_] = move + return self.base_moves[id_] def boost(self, stat: str, amount: int): self._boosts[stat] += amount @@ -170,13 +174,124 @@ def boost(self, stat: str, amount: int): elif self._boosts[stat] < -6: self._boosts[stat] = -6 - def cant_move(self): + def cant_move(self, move: Optional[str] = None): + if move: + self._add_move(move) self._first_turn = False self._protect_counter = 0 - if self._status == Status.SLP: self._status_counter += 1 + def check_consistency(self, pkmn_request: Dict[str, Any], player_role: str): + assert ( + pkmn_request["ident"] == f"{player_role}: {self.name}" + ), f"{pkmn_request['ident']} != {player_role}: {self.name}\nrequest: {pkmn_request}" + split_details = pkmn_request["details"].split(", ") + level = None + gender = None + shiny = split_details[-1] == "shiny" + if shiny: + split_details.pop() + if len(split_details) == 3: + _, level, gender = split_details + elif len(split_details) == 2: + if split_details[1].startswith("L"): + _, level = split_details + else: + _, gender = split_details + level = int(level[1:]) if level is not None else 100 + gender = ( + PokemonGender.from_request_details(gender) + if gender is not None + else PokemonGender.NEUTRAL + ) + assert level == self.level, f"{level} != {self.level}\nrequest: {pkmn_request}" + assert self.gender is not None + assert ( + gender == self.gender + ), f"{gender.name.lower()} != {self.gender.name.lower()}\nrequest: {pkmn_request}" + assert shiny == self.shiny, f"{shiny} != {self.shiny}\nrequest: {pkmn_request}" + assert ( + pkmn_request["active"] == self.active + ), f"{pkmn_request['active']} != {self.active}\nrequest: {pkmn_request}" + if self.item == "unknown_item": + # needed for item initialization in start of game, + # done anyway in update_from_request() + self._item = pkmn_request["item"] + if self.gen > 4: + assert pkmn_request["item"] == ( + self.item or "" + ), f"{pkmn_request['item']} != {self.item or ''}" + assert len(self.moves) <= 4, f"More than 4 moves: {self.moves}" + if self.base_species == "ditto": + return + assert ( + pkmn_request["condition"] == self.hp_status + ), f"{pkmn_request['condition']} != {self.hp_status}\nrequest: {pkmn_request}" + if self.base_species == "mew": + return + if not ( + # only check moves if mimic hasn't copied a move yet, + # or if mimic copies a move not already in the moveset + self._mimic_move is not None + and self._mimic_move.id in [m.id for m in self._moves.values()] + ): + for move_request, move in zip(pkmn_request["moves"], self.moves.values()): + assert Move.retrieve_id(move_request) == Move.retrieve_id( + move.id + ), f"{Move.retrieve_id(move_request)} != {Move.retrieve_id(move.id)}\nrequest: {pkmn_request}" + if self.ability is None: + # needed for ability initialization in start of game, + # done anyway in update_from_request() + self.ability = pkmn_request["baseAbility"] + assert pkmn_request["baseAbility"] == ( + self.base_ability or "" + ), f"{pkmn_request['baseAbility']} != {self.base_ability or ''}" + if "ability" in pkmn_request: + assert pkmn_request["ability"] == ( + self.ability or "" + ), f"{pkmn_request['ability']} != {self.ability or ''}" + + def check_move_consistency(self, active_request: Dict[str, Any]): + if self.base_species in ["ditto", "mew"]: + return + for move_request in active_request["moves"]: + matches = [ + m + for m in self.moves.values() + if Move.retrieve_id(m.id) == move_request["id"] + ] + assert len(matches) <= 1 + if not matches: + continue + move = matches[0] + if "pp" in move_request and self.gen not in [1, 2, 3, 7, 8]: + # exclude early gens because of unreliable Showdown event messages + # exclude gen 7 and 8 because of Z-move and Max Move PP untrackability + assert ( + move_request["pp"] == move.current_pp + ), f"{move_request['pp']} != {move.current_pp}\n{move_request}" + if "maxpp" in move_request: + assert ( + move_request["maxpp"] == move.max_pp + ), f"{move_request['maxpp']} != {move.max_pp}" + assert move.target is not None + if "target" in move_request: + target_name = ( + Target.SELF.name + if move.non_ghost_target and PokemonType.GHOST not in self.types + else ( + Target.ALL_ADJACENT_FOES.name + if move.id == "terastarstorm" + and self.type_1 == PokemonType.STELLAR + else move.target.name + ) + ) + assert ( + Target.from_showdown_message(move_request["target"]).name + == target_name + ), f"{Target.from_showdown_message(move_request['target']).name} != {target_name}\n{move_request}" + def clear_active(self): self._active = False @@ -267,6 +382,9 @@ def faint(self): self._current_hp = 0 self._status = Status.FNT self.temporary_ability = None + self._temporary_base_stats = None + self._transform_moves = None + self._mimic_move = None self._clear_effects() def forme_change(self, species: str): @@ -295,13 +413,24 @@ def mega_evolve(self, stone: str): mega_species = mega_species + stone[-1].lower() self._update_from_pokedex(mega_species, store_species=False) - def moved(self, move_id: str, failed: bool = False, use: bool = True): + def moved( + self, + move_id: str, + failed: bool = False, + use: bool = True, + reveal: bool = True, + pressure: bool = False, + ): self._must_recharge = False self._preparing_move = None self._preparing_target = None - move = self._add_move(move_id, use=use) + move = None + if reveal: + move = self._add_move(move_id) + if move is not None and use: + move.use(pressure) - if move and move.is_protect_counter and not failed: + if move is not None and move.is_protect_counter and not failed: self._protect_counter += 1 else: self._protect_counter = 0 @@ -309,23 +438,6 @@ def moved(self, move_id: str, failed: bool = False, use: bool = True): if self._status == Status.SLP: self._status_counter += 1 - if len(self._moves) > 4: - new_moves = {} - - # Keep the current move - if move and move in self._moves.values(): - new_moves = { - move_id: m for move_id, m in self._moves.items() if m is move - } - - for move_name in self._moves: - if len(new_moves) == 4: - break - elif move_name not in new_moves: - new_moves[move_name] = self._moves[move_name] - - self._moves = new_moves - # Handle silent effect ending if Effect.GLAIVE_RUSH in self.effects: self.end_effect("Glaive Rush") @@ -347,13 +459,11 @@ def moved(self, move_id: str, failed: bool = False, use: bool = True): self.end_effect("Flash Fire") def prepare(self, move_id: str, target: Optional[Pokemon]): - self.moved(move_id, use=False) - move_id = Move.retrieve_id(move_id) - move = self.moves[move_id] - - self._preparing_move = move - self._preparing_target = target + if move_id in self.moves: + move = self.moves[move_id] + self._preparing_move = move + self._preparing_target = target def primal(self): species_id_str = to_id_str(self._species) @@ -452,7 +562,10 @@ def switch_out(self, fields: Dict[Field, int]): self._preparing_target = None self._protect_counter = 0 self.temporary_ability = None + self._temporary_base_stats = None + self._transform_moves = None self._temporary_types = [] + self._mimic_move = None if self._status == Status.TOX: self._status_counter = 0 @@ -463,9 +576,16 @@ def terastallize(self, type_: str): self._temporary_types = [] def transform(self, into: Pokemon): - current_hp = self.current_hp - self._update_from_pokedex(into.species, store_species=False) - self._current_hp = int(current_hp) + dex_entry = GenData.from_gen(self.gen).pokedex[into.species] + self._heightm = dex_entry["heightm"] + self._weightkg = dex_entry["weightkg"] + self._temporary_base_stats = dex_entry["baseStats"] + if into.ability is not None: + self.ability = into.ability + self._temporary_types = [PokemonType.from_name(t) for t in dex_entry["types"]] + self._transform_moves = {m.id: Move(m.id, m.gen) for m in into.moves.values()} + for m in self._transform_moves.values(): + m._current_pp = 5 self._boosts = into.boosts.copy() def _update_from_pokedex(self, species: str, store_species: bool = True): @@ -491,7 +611,7 @@ def _update_from_pokedex(self, species: str, store_species: bool = True): self._possible_abilities = [ to_id_str(ability) for ability in dex_entry["abilities"].values() ] - if len(self._possible_abilities) == 1: + if len(self._possible_abilities) == 1 and self.gen >= 3: self.ability = self._possible_abilities[0] else: self.forme_change_ability = None @@ -549,7 +669,7 @@ def _update_from_details(self, details: str): def update_from_request(self, request_pokemon: Dict[str, Any]): self._active = request_pokemon["active"] - if request_pokemon == self._last_request: + if not request_pokemon["active"] and request_pokemon == self._last_request: return if self.ability is None: @@ -574,79 +694,10 @@ def update_from_request(self, request_pokemon: Dict[str, Any]): for move in request_pokemon["moves"]: self._add_move(move) - if len(self._moves) > 4: - moves_to_keep = { - Move.retrieve_id(move_id) for move_id in request_pokemon["moves"] - } - self._moves = { - move_id: move - for move_id, move in self._moves.items() - if move_id in moves_to_keep - } - if "stats" in request_pokemon: for stat in request_pokemon["stats"]: self._stats[stat] = request_pokemon["stats"][stat] - def check_consistency(self, pkmn_request: Dict[str, Any], player_role: str): - assert ( - pkmn_request["ident"] == f"{player_role}: {self.name}" - ), f"{pkmn_request['ident']} != {player_role}: {self.name}\nrequest: {pkmn_request}" - split_details = pkmn_request["details"].split(", ") - level = None - gender = None - shiny = split_details[-1] == "shiny" - if shiny: - split_details.pop() - if len(split_details) == 3: - _, level, gender = split_details - elif len(split_details) == 2: - if split_details[1].startswith("L"): - _, level = split_details - else: - _, gender = split_details - level = int(level[1:]) if level is not None else 100 - gender = ( - PokemonGender.from_request_details(gender) - if gender is not None - else PokemonGender.NEUTRAL - ) - assert level == self.level, f"{level} != {self.level}\nrequest: {pkmn_request}" - assert self.gender is not None - assert ( - gender == self.gender - ), f"{gender.name.lower()} != {self.gender.name.lower()}\nrequest: {pkmn_request}" - assert shiny == self.shiny, f"{shiny} != {self.shiny}\nrequest: {pkmn_request}" - assert ( - pkmn_request["active"] == self.active - ), f"{pkmn_request['active']} != {self.active}\nrequest: {pkmn_request}" - if self.item == "unknown_item": - self._item = pkmn_request["item"] - if self.gen > 4: - assert pkmn_request["item"] == ( - self.item or "" - ), f"{pkmn_request['item']} != {self.item or ''}" - if self.base_species == "ditto": - return - assert ( - pkmn_request["condition"] == self.hp_status - ), f"{pkmn_request['condition']} != {self.hp_status}\nrequest: {pkmn_request}" - if self.base_species == "mew": - return - for move_request, move in zip(pkmn_request["moves"], self.moves.values()): - assert Move.retrieve_id(move_request) == Move.retrieve_id( - move.id - ), f"{Move.retrieve_id(move_request)} != {Move.retrieve_id(move.id)}\nrequest: {pkmn_request}" - if self.ability is None: - self.ability = pkmn_request["baseAbility"] - assert pkmn_request["baseAbility"] == ( - self.base_ability or "" - ), f"{pkmn_request['baseAbility']} != {self.base_ability or ''}" - if "ability" in pkmn_request: - assert pkmn_request["ability"] == ( - self.ability or "" - ), f"{pkmn_request['ability']} != {self.ability or ''}" - def _update_from_teambuilder(self, tb: TeambuilderPokemon): if tb.nickname is not None and tb.species is None: self._update_from_pokedex(tb.nickname) @@ -818,11 +869,11 @@ def available_z_moves(self) -> List[Move]: if type_: return [ move - for move in self._moves.values() + for move in self.moves.values() if move.type == type_ and move.can_z_move ] - elif move in self._moves: - return [self._moves[move]] + elif move in self.moves: + return [self.moves[move]] return [] @property @@ -833,6 +884,17 @@ def base_ability(self) -> Optional[str]: """ return self._forme_change_ability or self._ability + @property + def base_moves(self) -> Dict[str, Move]: + """ + :return: The pokemon's underlying move dictionary. When transformed, this + returns the temporary move set; otherwise it returns the learned moves. + :rtype: Dict[str, Move] + """ + return ( + self._transform_moves if self._transform_moves is not None else self._moves + ) + @property def base_species(self) -> str: """ @@ -848,7 +910,11 @@ def base_stats(self) -> Dict[str, int]: :return: The pokemon's base stats. :rtype: Dict[str, int] """ - return self._base_stats + return ( + self._temporary_base_stats + if self._temporary_base_stats is not None + else self._base_stats + ) @property def boosts(self) -> Dict[str, int]: @@ -907,6 +973,18 @@ def first_turn(self) -> bool: """ return self._first_turn + @property + def forme_change_ability(self) -> Optional[str]: + """ + :return: The pokemon's ability after changing forme. None if the pokemon hasn't changed forme. + :rtype: str, optional + """ + return self._forme_change_ability + + @forme_change_ability.setter + def forme_change_ability(self, ability: Optional[str]): + self._forme_change_ability = to_id_str(ability) if ability is not None else None + @property def gen(self) -> int: """ @@ -996,25 +1074,22 @@ def max_hp(self) -> int: """ return self._max_hp or 0 - @property - def forme_change_ability(self) -> Optional[str]: - """ - :return: The pokemon's ability after changing forme. None if the pokemon hasn't changed forme. - :rtype: str, optional - """ - return self._forme_change_ability - - @forme_change_ability.setter - def forme_change_ability(self, ability: Optional[str]): - self._forme_change_ability = to_id_str(ability) if ability is not None else None - @property def moves(self) -> Dict[str, Move]: """ :return: A dictionary of the pokemon's known moves. :rtype: Dict[str, Move] """ - return self._moves + moves = self.base_moves + if self._mimic_move is not None: + return dict( + [ + (k, v) if k != "mimic" else (self._mimic_move.id, self._mimic_move) + for k, v in moves.items() + ] + ) + else: + return moves @property def must_recharge(self) -> bool: diff --git a/strict_integration_tests/test_strict.py b/strict_integration_tests/test_strict.py index a13d3d22e..9212bf7ef 100644 --- a/strict_integration_tests/test_strict.py +++ b/strict_integration_tests/test_strict.py @@ -2,7 +2,14 @@ import pytest -from poke_env.player import RandomPlayer, cross_evaluate +from poke_env.player import ForfeitBattleOrder, RandomPlayer, cross_evaluate + + +class ZoroarkForfeitRandomPlayer(RandomPlayer): + def choose_move(self, battle): + if "illusion" in [p.ability for p in battle.team.values()]: + return ForfeitBattleOrder() + return super().choose_move(battle) async def simple_cross_evaluation(n_battles, players): @@ -15,13 +22,17 @@ async def simple_cross_evaluation(n_battles, players): @pytest.mark.asyncio async def test_random_players(): - for gen in range(4, 10): + for gen in range(1, 10): players = [ - RandomPlayer( - battle_format=f"gen{gen}randombattle", strict_battle_tracking=True + ZoroarkForfeitRandomPlayer( + battle_format=f"gen{gen}randombattle", + max_concurrent_battles=10, + strict_battle_tracking=True, ), - RandomPlayer( - battle_format=f"gen{gen}randombattle", strict_battle_tracking=True + ZoroarkForfeitRandomPlayer( + battle_format=f"gen{gen}randombattle", + max_concurrent_battles=10, + strict_battle_tracking=True, ), ] await asyncio.wait_for( diff --git a/unit_tests/environment/test_battle.py b/unit_tests/environment/test_battle.py index 71ef4fe4d..b805df6aa 100644 --- a/unit_tests/environment/test_battle.py +++ b/unit_tests/environment/test_battle.py @@ -358,6 +358,7 @@ def test_battle_request_and_interactions(example_request): battle.parse_message(["", "-mustrecharge", "p1: Latias"]) assert battle.opponent_active_pokemon.must_recharge is True + battle.opponent_active_pokemon._add_move("solarbeam") battle.parse_message(["", "-prepare", "p1: Latias", "Solar Beam", "p2: Necrozma"]) assert ( battle.opponent_active_pokemon.preparing_move @@ -365,12 +366,6 @@ def test_battle_request_and_interactions(example_request): ) assert battle.opponent_active_pokemon.preparing_target.species == "necrozma" - assert ( - battle.opponent_active_pokemon.preparing_move - == battle.opponent_active_pokemon.moves["solarbeam"] - ) - assert battle.opponent_active_pokemon.preparing_target.species == "necrozma" - battle.parse_message(["", "switch", "p1: Groudon", "Groudon, L82", "100/100"]) battle.parse_message(["", "-primal", "p1: Groudon"]) assert battle.opponent_active_pokemon.species == "groudon" diff --git a/unit_tests/environment/test_pokemon.py b/unit_tests/environment/test_pokemon.py index 818de65e0..e9693757b 100644 --- a/unit_tests/environment/test_pokemon.py +++ b/unit_tests/environment/test_pokemon.py @@ -77,6 +77,7 @@ def test_pokemon_damage_multiplier(): def test_powerherb_ends_move_preparation(): mon = Pokemon(species="roserade", gen=8) + mon._add_move("solarbeam") mon.item = "powerherb" mon.prepare("solarbeam", None)