DoubleBattle.get_possible_showdown_targets(), performance cleanup
This method gets called once per move per active slot every turn, roughly 8 times per turn in a standard doubles battle. I profiled it and found three places where it was doing the same work over and over for no reason. None of this touches the public API or changes what the method returns.
Benchmarks (500 iterations × ~200 live call states against a random opponent):
- Before: ~21.8 µs/call
- After: ~3.1 µs/call
- ~7× faster
At 8 calls/turn across 8 concurrent battle environments, that's about 1.2 ms saved per training step.
1. Slot detection was building two lists just to do a membership check (lines 323–334)
# Before
if pokemon == pokemon_1 and move.id in [m.id for m in self.available_moves[0]]:
...
elif pokemon == pokemon_2 and move.id in [m.id for m in self.available_moves[1]]:
The method already unpacks self.active_pokemon into (pokemon_1, pokemon_2). Pokemon objects are unique by identity, so an identity check is enough:
if pokemon is pokemon_1:
slot_idx = 0
elif pokemon is pokemon_2:
slot_idx = 1
else:
raise Exception(...)
2. The target dispatch dict was being rebuilt from scratch every single call, 14 regex operations each time (lines 356–394)
# Before
targets = {
Target.from_showdown_message("adjacentAlly"): [ally_position],
Target.from_showdown_message("adjacentFoe"): [...],
...
}[move.deduced_target]
The keys are all Target enum members. The values only depend on which slot is acting, and DoubleBattle's position constants are fixed at the class level. So we can just precompute both slot variants once, at module load time:
_TARGET_POSITIONS: tuple = (
{Target.ADJACENT_ALLY: [POKEMON_2_POSITION], Target.NORMAL: [POKEMON_2_POSITION, ...], ...}, # slot 0
{Target.ADJACENT_ALLY: [POKEMON_1_POSITION], Target.NORMAL: [POKEMON_1_POSITION, ...], ...}, # slot 1
)
# Usage
targets = _TARGET_POSITIONS[slot_idx][move.deduced_target]
This gets rid of all 14 Target.from_showdown_message() calls on every invocation.
3. The occupancy filter dict was being rebuilt with f-strings on every call (lines 396–406)
# Before
targets_to_keep = {
{
f"{self.player_role}a": -1,
f"{self.player_role}b": -2,
f"{self.opponent_role}a": 1,
f"{self.opponent_role}b": 2,
}[pokemon_identifier]
for pokemon_identifier in pokemon_ids
}
player_role and opponent_role are set once when the battle starts and never change. Cache the mapping on the instance on first use:
if self._identifier_to_position is None:
self._identifier_to_position = {
f"{self.player_role}a": self.POKEMON_1_POSITION,
f"{self.player_role}b": self.POKEMON_2_POSITION,
f"{self.opponent_role}a": self.OPPONENT_1_POSITION,
f"{self.opponent_role}b": self.OPPONENT_2_POSITION,
}
id2pos = self._identifier_to_position
targets_to_keep = {id2pos[pid] for pid in pokemon_ids}
Compatibility notes
- Method signature and return type unchanged
- No other methods or classes affected
Note
This is marginal compared to the cost of back and forth with showdown, but I think it's worth it anyway
DoubleBattle.get_possible_showdown_targets(), performance cleanupThis method gets called once per move per active slot every turn, roughly 8 times per turn in a standard doubles battle. I profiled it and found three places where it was doing the same work over and over for no reason. None of this touches the public API or changes what the method returns.
Benchmarks (500 iterations × ~200 live call states against a random opponent):
At 8 calls/turn across 8 concurrent battle environments, that's about 1.2 ms saved per training step.
1. Slot detection was building two lists just to do a membership check (lines 323–334)
The method already unpacks
self.active_pokemoninto(pokemon_1, pokemon_2). Pokemon objects are unique by identity, so an identity check is enough:2. The target dispatch dict was being rebuilt from scratch every single call, 14 regex operations each time (lines 356–394)
The keys are all
Targetenum members. The values only depend on which slot is acting, andDoubleBattle's position constants are fixed at the class level. So we can just precompute both slot variants once, at module load time:This gets rid of all 14
Target.from_showdown_message()calls on every invocation.3. The occupancy filter dict was being rebuilt with f-strings on every call (lines 396–406)
player_roleandopponent_roleare set once when the battle starts and never change. Cache the mapping on the instance on first use:Compatibility notes
Note
This is marginal compared to the cost of back and forth with showdown, but I think it's worth it anyway