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)