Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 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
25bf950
sync up with new master
cameronangliss Feb 2, 2026
a2be0e2
avoid indexing failure
cameronangliss Feb 2, 2026
9c899f7
simplify imports
cameronangliss Feb 2, 2026
20a1a4f
use property
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
117 changes: 85 additions & 32 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 @@ -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:
Expand All @@ -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",
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]",
}:
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,
Expand All @@ -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:
Expand All @@ -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:",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions src/poke_env/battle/battle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]

Expand Down
42 changes: 42 additions & 0 deletions src/poke_env/battle/double_battle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
26 changes: 21 additions & 5 deletions src/poke_env/battle/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,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 @@ -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()])
Expand All @@ -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:
Expand Down Expand Up @@ -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]:
Expand Down
Loading
Loading