Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
68fcb9d
initial attempt
cameronangliss Jan 1, 2026
19a67fd
bugfix
cameronangliss Jan 2, 2026
69ecb45
get rid of unused EmptyMove
cameronangliss Jan 2, 2026
531bb23
fix tests
cameronangliss Jan 2, 2026
1fb0d39
black
cameronangliss Jan 2, 2026
bf481a6
simplify
cameronangliss Jan 2, 2026
0949aa4
black
cameronangliss Jan 2, 2026
24e1981
fix typo
cameronangliss Jan 2, 2026
b231155
cleanup
cameronangliss Jan 2, 2026
994fdab
restore
cameronangliss Jan 2, 2026
58eacf1
further fix tracking issues
cameronangliss Jan 5, 2026
0cef241
fix mypy
cameronangliss Jan 5, 2026
a7acc7d
Merge branch 'master' of github.com:cameronangliss/poke-env into trac…
cameronangliss Jan 5, 2026
4add818
cleanup
cameronangliss Jan 5, 2026
bd124c9
fix tera starstorm targeting checking
cameronangliss Jan 5, 2026
491b108
track leppa berry consumption
cameronangliss Jan 5, 2026
a3804c4
avoid troublesome Copycat move tracking
cameronangliss Jan 5, 2026
46e89e7
clip pp under max pp
cameronangliss Jan 5, 2026
fd6e247
don't reveal on lockedmove
cameronangliss Jan 5, 2026
631769f
simplify logic and improve variable name
cameronangliss Jan 6, 2026
ae97244
black
cameronangliss Jan 6, 2026
cd0d901
fix
cameronangliss Jan 6, 2026
4c64ecd
fix format name
cameronangliss Jan 6, 2026
a62b65d
typo
cameronangliss Jan 6, 2026
b6dcef6
remove unneeded field
cameronangliss Jan 14, 2026
85458a7
fix format name
cameronangliss Jan 14, 2026
0f578fa
Merge branch 'fix-format-name' of github.com:cameronangliss/poke-env …
cameronangliss Jan 14, 2026
1d2fec5
Merge branch 'master' of github.com:cameronangliss/poke-env into trac…
cameronangliss Jan 29, 2026
05433db
do pp bounding at specific pp changing code locations
cameronangliss Jan 30, 2026
70916af
fix mypy
cameronangliss Jan 30, 2026
6c56267
centralize gen control
cameronangliss Jan 30, 2026
7fd69fa
more specific consistency checking give-up
cameronangliss Jan 30, 2026
45f3dd2
assert not more than 1 match
cameronangliss Jan 30, 2026
4318f77
more specific give-up condition
cameronangliss Jan 30, 2026
ada8c73
limited expansion of strict testing to early gens
cameronangliss Jan 31, 2026
053807c
better comment
cameronangliss Jan 31, 2026
e8d907e
no copycat pp tracking
cameronangliss Jan 31, 2026
702800c
fix Trick tracking
cameronangliss Jan 31, 2026
7ce1dde
fix subtle pressure bug
cameronangliss Jan 31, 2026
5009bc1
fix override PP tracking
cameronangliss Jan 31, 2026
b9f02be
fix Transform move overwrite tracking
cameronangliss Jan 31, 2026
799f33a
complete transform tracking
cameronangliss Jan 31, 2026
e371053
fix
cameronangliss Jan 31, 2026
e3afdd5
relax conditions on consistency checking and remove unnecessary code
cameronangliss Jan 31, 2026
5e1393b
unrelax conditions
cameronangliss Jan 31, 2026
4cc2a11
actually fix Trick tracking
cameronangliss Feb 1, 2026
da08488
better Zoroark prevention in strict tests
cameronangliss Feb 1, 2026
e5a3dd0
simplify diff
cameronangliss Feb 1, 2026
87a3824
better comment
cameronangliss Feb 1, 2026
1b9d418
removing comment that is now misleading
cameronangliss Feb 1, 2026
aee76f4
Merge branch 'master' of github.com:cameronangliss/poke-env into trac…
cameronangliss Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fixture_data/teams.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"packed-format": "zuzu|azumarill|aguavberry|hugepower|aquajet,bellydrum,return,playrough|Adamant|44,252,18,4,18,172|F||S|67|211]metal bird|celesteela|leftovers|beastboost|protect,rockslide,heavyslam,leechseed|Impish|252,,72,,184,||,,,0,9,20|||144]Garchomp||choiceband|roughskin|rockslide,earthpower,outrage,aquatail|Jolly|8,252,,,,248|||S||]Greninja||choicespecs|protean|scald,icebeam,darkpulse,extrasensory|Timid|4,,,252,,252|M|29,0,3,6,21,23|||]Heatran||assaultvest|flashfire|overheat,flamethrower,flashcannon,earthpower|Modest|252,,,252,,4||,0,,,26,|||,Ice,]kaaaaaaa|kartana|focussash|beastboost|leafblade,sacredsword,swordsdance,xscissor|Jolly|4,252,,,,252||||62|200"
}
],
"gen9vgc2025regi": [
"gen9vgc2024regg": [
{
"showdown-file": "beastcoasttt-413XPlayz.showdown",
"packed-format": "Iron Hands||assaultvest|quarkdrive|fakeout,drainpunch,wildcharge,heavyslam|Adamant|4,156,4,,252,92||||50|,,,,,Water]Flutter Mane||boosterenergy|protosynthesis|moonblast,shadowball,icywind,protect|Timid|4,,196,148,4,156||,0,,,,||50|,,,,,Fairy]Landorus-Therian||choicescarf|intimidate|stompingtantrum,terablast,rockslide,uturn|Adamant|132,116,4,,4,252||||50|,,,,,Flying]Ogerpon-Wellspring||wellspringmask|waterabsorb|spikyshield,ivycudgel,hornleech,followme|Adamant|252,76,100,,12,68|F|||50|,,,,,Water]Chi-Yu||choicespecs|beadsofruin|heatwave,darkpulse,snarl,overheat|Timid|132,,4,108,12,252||,0,,,,||50|,,,,,Ghost]Sinistcha||rockyhelmet|hospitality|matchagotcha,ragepowder,trickroom,strengthsap|Bold|252,,252,4,,||,0,,,,||50|,,,,,Fairy"
Expand Down
3 changes: 1 addition & 2 deletions src/poke_env/battle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from poke_env.battle.double_battle import DoubleBattle
from poke_env.battle.effect import Effect
from poke_env.battle.field import Field
from poke_env.battle.move import SPECIAL_MOVES, EmptyMove, Move
from poke_env.battle.move import SPECIAL_MOVES, Move
from poke_env.battle.move_category import MoveCategory
from poke_env.battle.observation import Observation
from poke_env.battle.observed_pokemon import ObservedPokemon
Expand All @@ -21,7 +21,6 @@
"Battle",
"DoubleBattle",
"Effect",
"EmptyMove",
"Field",
"Move",
"MoveCategory",
Expand Down
85 changes: 59 additions & 26 deletions src/poke_env/battle/abstract_battle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -479,9 +480,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:
Expand All @@ -491,37 +493,40 @@ 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",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is sky attack still handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See here for what I yielded from my investigation: ada8c73. Basically, that message only seems to appear in random battles in early gens, so I added those to the strict integration tests and passed through them a few times to weed out existing early-gen bugs.

"[from]Pursuit",
"[zeffect]",
}:
if event[-1] in {"[from] lockedmove", "[from]lockedmove"}:
use = False
reveal = False
event = event[:-1]

if event[-1] in {"[from] Pursuit", "[from]Pursuit", "[zeffect]"}:
event = event[:-1]

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,
Expand All @@ -537,7 +542,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:
Expand All @@ -558,6 +564,7 @@ def parse_message(self, split_message: List[str]):
if event[-1] == "":
event = event[:-1]

presumed_target = None
if len(event) == 4:
pokemon, move = event[2:4]
elif len(event) == 5:
Expand Down Expand Up @@ -600,16 +607,35 @@ def parse_message(self, split_message: List[str]):
temp_pokemon = self.get_pokemon(pokemon)
temp_pokemon.start_effect("MINIMIZE")

if override_move:
pressure = self._pressure_on(pokemon, move, presumed_target)
mon = self.get_pokemon(pokemon)
if overridden_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)
mon.moved(move, failed=failed, use=False, reveal=reveal)
overriden = mon.moves[Move.retrieve_id(overridden_move)]
overriden.use(pressure)
else:
if not failed and move in {
"Sleep Talk",
"Copycat",
"Metronome",
"Nature Power",
}:
# wait until override move to decide how much pp to deduct
# since override determines pressure interaction
use = False
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
Expand Down Expand Up @@ -747,6 +773,9 @@ def parse_message(self, split_message: List[str]):
"[item] ", ""
)
self.get_pokemon(target).item = None
elif effect == "item: Leppa Berry":
move = to_id_str(event[4])
self.get_pokemon(target).moves[move].current_pp += 10
elif target != "": # ['', '-activate', '', 'move: Splash']
self.get_pokemon(target).start_effect(effect)
elif event[1] == "-status":
Expand Down Expand Up @@ -1040,6 +1069,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._data.gen)
Expand Down
39 changes: 39 additions & 0 deletions src/poke_env/battle/battle.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ def parse_request(
self._trapped = True

if self.active_pokemon is not None:
if (
strict_battle_tracking
and self.gen not in [7, 8]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity - why only these gens?

Copy link
Contributor Author

@cameronangliss cameronangliss Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the give-up for move consistency checking more specific here: 7fd69fa. The reason why we need to do this is because the mapping from moves to Z-moves or Dynamax moves is not invertible (2 distinct moves may have the same name for their z-move or dynamax move). Thus, it is impossible to track pp changes in those circumstances, so I have the code just not check that in gen 7 and 8, but only for pp consistency checking now.

and "illusion" not in [p.ability for p in self.team.values()]
and "illusion"
not in [p.ability for p in self.opponent_team.values()]
):
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:
Expand Down Expand Up @@ -125,6 +133,37 @@ 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:
if self.gen == 7:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the if-statement more specific and added an explaining comment: 4318f77

return False
move_data = self._data.moves[Move.retrieve_id(move)]
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]

Expand Down
40 changes: 40 additions & 0 deletions src/poke_env/battle/double_battle.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ def parse_request(
force_self_team=True,
details=pokemon_dict["details"],
)
if (
strict_battle_tracking
and self.gen not in [7, 8]
and "illusion" not in [p.ability for p in self.team.values()]
and "illusion"
not in [p.ability for p in self.opponent_team.values()]
):
active_pokemon.check_move_consistency(active_request)
if self.player_role is not None:
if (
active_pokemon_number == 0
Expand Down Expand Up @@ -210,6 +218,38 @@ 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_data = self._data.moves[Move.retrieve_id(move)]
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]
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 self._data.moves[Move.retrieve_id(move)]["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]
Expand Down
42 changes: 13 additions & 29 deletions src/poke_env/battle/move.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import copy
from functools import lru_cache
from typing import Any, Dict, List, Optional, Set, Tuple, Union

Expand All @@ -13,7 +12,6 @@
from poke_env.data import GenData, to_id_str

SPECIAL_MOVES: Set[str] = {"struggle", "recharge"}

_PROTECT_MOVES = {
"protect",
"detect",
Expand Down Expand Up @@ -80,7 +78,6 @@ class Move:
"_current_pp",
"_dynamaxed_move",
"_gen",
"_is_empty",
"_moves_dict",
"_request_target",
)
Expand All @@ -102,16 +99,18 @@ def __init__(self, move_id: str, gen: int, raw_id: Optional[str] = None):
pass

self._current_pp = self.max_pp
self._is_empty: bool = False

self._dynamaxed_move = None
self._request_target = None

def __repr__(self) -> str:
return f"{self._id} (Move object)"

def use(self):
self._current_pp -= 1
def use(self, pressure: bool = False):
if pressure:
self.current_pp -= 2
else:
self.current_pp -= 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we bound this at 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


@staticmethod
def is_id_z(id_: str, gen: int) -> bool:
Expand Down Expand Up @@ -219,6 +218,14 @@ def current_pp(self) -> int:
"""
return self._current_pp

@current_pp.setter
def current_pp(self, pp: int):
"""
:param pp: New PP value.
:type pp: int
"""
self._current_pp = min(max(0, pp), self.max_pp)

@property
def damage(self) -> Union[int, str]:
"""
Expand Down Expand Up @@ -402,14 +409,6 @@ def ignore_immunity(self) -> Union[bool, Set[PokemonType]]:
}
return False

@property
def is_empty(self) -> bool:
"""
:return: Whether the move is an empty move.
:rtype: bool
"""
return self._is_empty

@property
def is_protect_counter(self) -> bool:
"""
Expand Down Expand Up @@ -768,21 +767,6 @@ def z_move_power(self) -> int:
return 200


class EmptyMove(Move):
def __init__(self, move_id: str):
self._id = move_id
self._is_empty: bool = True

def __getattribute__(self, name: str):
try:
return super(Move, self).__getattribute__(name)
except (AttributeError, TypeError, ValueError):
return 0

def __deepcopy__(self, memodict: Optional[Dict[int, Any]] = {}):
return EmptyMove(copy.deepcopy(self._id, memodict))


class DynamaxMove(Move):
BOOSTS_MAP = {
PokemonType.BUG: {"spa": -1},
Expand Down
Loading
Loading