Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f9f894d

Browse files
committedMar 17, 2025·
initial attempt
1 parent 74f7d1b commit f9f894d

File tree

2 files changed

+188
-3
lines changed

2 files changed

+188
-3
lines changed
 

‎integration_tests/test_baselines.py

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ async def test_shp():
4242
await asyncio.wait_for(simple_cross_evaluation(5, players=players), timeout=5)
4343

4444

45+
@pytest.mark.asyncio
46+
async def test_shp_in_doubles():
47+
players = [
48+
RandomPlayer(battle_format="gen9randomdoublesbattle"),
49+
SimpleHeuristicsPlayer(battle_format="gen9randomdoublesbattle"),
50+
]
51+
await asyncio.wait_for(simple_cross_evaluation(5, players=players), timeout=5)
52+
53+
4554
@pytest.mark.asyncio
4655
async def test_max_base_power():
4756
players = [RandomPlayer(), MaxBasePowerPlayer()]

‎src/poke_env/player/baselines.py

+179-3
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,15 @@ def _stat_estimation(self, mon: Pokemon, stat: str):
180180

181181
def choose_move(self, battle: AbstractBattle):
182182
if isinstance(battle, DoubleBattle):
183-
return self.choose_random_doubles_move(battle)
183+
return self.choose_doubles_move(battle)
184184

185-
# Main mons shortcuts
185+
# Singles logic
186186
active = battle.active_pokemon
187187
opponent = battle.opponent_active_pokemon
188188

189189
if active is None or opponent is None:
190190
return self.choose_random_move(battle)
191191

192-
# Rough estimation of damage ratio
193192
physical_ratio = self._stat_estimation(active, "atk") / self._stat_estimation(
194193
opponent, "def"
195194
)
@@ -270,3 +269,180 @@ def choose_move(self, battle: AbstractBattle):
270269
)
271270

272271
return self.choose_random_move(battle)
272+
273+
def choose_doubles_move(self, battle: DoubleBattle):
274+
if any(battle.force_switch):
275+
return self.choose_random_doubles_move(battle)
276+
277+
orders: List[Optional[BattleOrder]] = []
278+
switched_in = None
279+
280+
# For convenience, alias opponent active Pokémon
281+
opp1 = (
282+
battle.opponent_active_pokemon[0]
283+
if len(battle.opponent_active_pokemon) > 0
284+
else None
285+
)
286+
opp2 = (
287+
battle.opponent_active_pokemon[1]
288+
if len(battle.opponent_active_pokemon) > 1
289+
else None
290+
)
291+
292+
for mon, moves, switches in zip(
293+
battle.active_pokemon, battle.available_moves, battle.available_switches
294+
):
295+
if mon is None or mon.fainted:
296+
orders.append(None)
297+
continue
298+
299+
filtered_switches = [s for s in switches if s != switched_in]
300+
301+
# Compute worst-case matchup against both opponents
302+
score1 = (
303+
self._estimate_matchup(mon, opp1) if opp1 and not opp1.fainted else 0
304+
)
305+
score2 = (
306+
self._estimate_matchup(mon, opp2) if opp2 and not opp2.fainted else 0
307+
)
308+
worst_matchup = min(score1, score2)
309+
310+
# If matchup is poor and there is a better switch option, switch out.
311+
if filtered_switches and worst_matchup < self.SWITCH_OUT_MATCHUP_THRESHOLD:
312+
best_switch = max(
313+
filtered_switches,
314+
key=lambda s: min(
315+
(
316+
self._estimate_matchup(s, opp1)
317+
if opp1 and not opp1.fainted
318+
else float("-inf")
319+
),
320+
(
321+
self._estimate_matchup(s, opp2)
322+
if opp2 and not opp2.fainted
323+
else float("-inf")
324+
),
325+
),
326+
)
327+
orders.append(BattleOrder(best_switch))
328+
switched_in = best_switch
329+
continue
330+
331+
# Evaluate moves with heuristics tuned for doubles:
332+
if moves:
333+
best_move = None
334+
best_score = float("-inf")
335+
for move in moves:
336+
# Check if the move can hit both opponents
337+
double_target = (
338+
move.target in {Target.NORMAL, Target.ANY}
339+
and opp1
340+
and opp2
341+
and not opp1.fainted
342+
and not opp2.fainted
343+
)
344+
move_power = move.base_power * (1.5 if double_target else 1)
345+
346+
# For damage ratios, compute estimates against both opponents and take the minimum.
347+
if move.category == MoveCategory.PHYSICAL:
348+
ratio1 = self._stat_estimation(mon, "atk") / (
349+
self._stat_estimation(opp1, "def")
350+
if opp1 and not opp1.fainted
351+
else 1
352+
)
353+
ratio2 = self._stat_estimation(mon, "atk") / (
354+
self._stat_estimation(opp2, "def")
355+
if opp2 and not opp2.fainted
356+
else 1
357+
)
358+
elif move.category == MoveCategory.SPECIAL:
359+
ratio1 = self._stat_estimation(mon, "spa") / (
360+
self._stat_estimation(opp1, "spd")
361+
if opp1 and not opp1.fainted
362+
else 1
363+
)
364+
ratio2 = self._stat_estimation(mon, "spa") / (
365+
self._stat_estimation(opp2, "spd")
366+
if opp2 and not opp2.fainted
367+
else 1
368+
)
369+
else:
370+
ratio1 = ratio2 = 1
371+
move_ratio = min(ratio1, ratio2)
372+
373+
# Combine the factors: base power, ratio, accuracy, expected hits,
374+
# and the worst-case damage multiplier against opponents.
375+
base_score = (
376+
move_power
377+
* move_ratio
378+
* move.accuracy
379+
* move.expected_hits
380+
* min(
381+
(
382+
opp1.damage_multiplier(move)
383+
if opp1 and not opp1.fainted
384+
else 1
385+
),
386+
(
387+
opp2.damage_multiplier(move)
388+
if opp2 and not opp2.fainted
389+
else 1
390+
),
391+
)
392+
)
393+
394+
# Bonus for setup moves that boost stats significantly.
395+
if (
396+
move.boosts
397+
and sum(move.boosts.values()) >= 2
398+
and move.target == "self"
399+
):
400+
base_score *= 1.2
401+
402+
if base_score > best_score:
403+
best_score = base_score
404+
best_move = move
405+
406+
if best_move:
407+
targets = battle.get_possible_showdown_targets(best_move, mon)
408+
if best_move.target in {Target.NORMAL, Target.ANY}:
409+
possible_targets = [
410+
t
411+
for t in targets
412+
if t
413+
in {battle.OPPONENT_1_POSITION, battle.OPPONENT_2_POSITION}
414+
]
415+
target = (
416+
random.choice(possible_targets)
417+
if possible_targets
418+
else random.choice(targets)
419+
)
420+
else:
421+
target = random.choice(targets)
422+
orders.append(BattleOrder(best_move, move_target=target))
423+
continue
424+
425+
# Fallback: if no moves are available, switch if possible.
426+
if filtered_switches:
427+
best_switch = max(
428+
filtered_switches,
429+
key=lambda s: min(
430+
(
431+
self._estimate_matchup(s, opp1)
432+
if opp1 and not opp1.fainted
433+
else float("-inf")
434+
),
435+
(
436+
self._estimate_matchup(s, opp2)
437+
if opp2 and not opp2.fainted
438+
else float("-inf")
439+
),
440+
),
441+
)
442+
orders.append(BattleOrder(best_switch))
443+
else:
444+
orders.append(DefaultBattleOrder())
445+
446+
if orders and len(orders) >= 2:
447+
return DoubleBattleOrder(orders[0], orders[1])
448+
return self.choose_random_doubles_move(battle)

0 commit comments

Comments
 (0)
Please sign in to comment.