106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Iterable, List, Optional
|
|
|
|
from .battle import Battle
|
|
from .unit import TEAM_ENEMY, TEAM_PLAYER, Unit
|
|
|
|
|
|
def _ai_act(battle: Battle, unit: Unit) -> bool:
|
|
"""Let `unit` take an action using the engine's AI (mirrors the player's
|
|
auto-battle behaviour). Returns True if it used a skill."""
|
|
if unit.team == TEAM_ENEMY:
|
|
return battle.enemy_ai(unit)
|
|
s = battle.best_skill(unit)
|
|
if s is None:
|
|
return False
|
|
foes = battle.foes_of(unit)
|
|
if s.kind == "heal":
|
|
low = [a for a in battle.allies_of(unit) if a.hp < a.max_hp]
|
|
return battle.use_skill(unit, s, min(low, key=lambda a: a.hp) if low else None)
|
|
if s.kind == "shield":
|
|
t = unit if s.target == "self" else min(battle.allies_of(unit), key=lambda a: a.hp)
|
|
return battle.use_skill(unit, s, t)
|
|
if s.target in ("all_enemies", "two_enemies"):
|
|
return battle.use_skill(unit, s, None)
|
|
if s.target == "any":
|
|
tg = battle.valid_skill_targets(unit, s)
|
|
return battle.use_skill(unit, s, min(tg, key=lambda t: t.hp) if tg else None)
|
|
if s.kind == "utility" and s.mechanic == "reset_cooldowns":
|
|
al = battle.allies_of(unit)
|
|
if al:
|
|
target = max(al, key=lambda a: sum(x.cooldown_left for x in a.skills))
|
|
return battle.use_skill(unit, s, target)
|
|
return False
|
|
if s.target in ("ally", "all_allies"):
|
|
al = battle.allies_of(unit)
|
|
return battle.use_skill(unit, s, min(al, key=lambda a: a.hp) if al else None)
|
|
tg = battle.valid_skill_targets(unit, s)
|
|
if not tg:
|
|
return False
|
|
if s.mechanic == "hits_per_effect":
|
|
t = max(tg, key=lambda t: (len([k for k in t.status if k != "form"]), -t.hp))
|
|
else:
|
|
t = min(tg, key=lambda t: t.hp)
|
|
return battle.use_skill(unit, s, t)
|
|
|
|
|
|
def play_to_end(battle: Battle) -> Optional[int]:
|
|
"""Play `battle` to completion with the AI controlling both sides.
|
|
Returns the winning team (TEAM_PLAYER / TEAM_ENEMY)."""
|
|
battle.start()
|
|
guard = 0
|
|
while battle.winner is None and battle.current is not None and guard < 600:
|
|
guard += 1
|
|
unit = battle.current
|
|
if not unit.acted:
|
|
used = _ai_act(battle, unit)
|
|
if not used:
|
|
battle.next_turn()
|
|
continue
|
|
if battle.current is unit and unit.acted:
|
|
battle.next_turn()
|
|
return battle.winner
|
|
|
|
|
|
def _surviving_hp_fraction(battle: Battle) -> float:
|
|
if not battle.players:
|
|
return 0.0
|
|
return sum(u.hp / u.max_hp for u in battle.players) / len(battle.players)
|
|
|
|
|
|
def estimate_dominance(player_units: List[Unit], enemy_units: List[Unit],
|
|
relics: Iterable[str] = (), runs: int = 25,
|
|
win_thresh: float = 0.99, margin_thresh: float = 0.6) -> bool:
|
|
"""Monte-Carlo estimate of whether the player team crushes the enemy team.
|
|
|
|
Returns True only for confident blowouts: the player wins almost every run
|
|
AND ends with most of the team at high health. Used to decide whether a
|
|
"Finish battle" button is safe to show (it's hidden the moment the matchup
|
|
is remotely close)."""
|
|
wins = 0
|
|
margin = 0.0
|
|
for i in range(runs):
|
|
b = Battle(copy.deepcopy(player_units), copy.deepcopy(enemy_units),
|
|
relics=list(relics))
|
|
winner = play_to_end(b)
|
|
if winner == TEAM_PLAYER:
|
|
wins += 1
|
|
margin += _surviving_hp_fraction(b)
|
|
# early exit: even if we win every remaining run we can't hit the threshold
|
|
if wins + (runs - i - 1) < runs * win_thresh:
|
|
return False
|
|
if runs == 0:
|
|
return False
|
|
return wins / runs >= win_thresh and margin / max(1, wins) >= margin_thresh
|
|
|
|
|
|
def resolve_instantly(player_units: List[Unit], enemy_units: List[Unit],
|
|
relics: Iterable[str] = ()) -> Optional[int]:
|
|
"""Run a single real simulation to completion and return its outcome. This
|
|
is what the 'Finish battle' button applies, so the result is genuine, not
|
|
an assumed win."""
|
|
b = Battle(copy.deepcopy(player_units), copy.deepcopy(enemy_units),
|
|
relics=list(relics))
|
|
return play_to_end(b) |