Initial commit: Plants turn-based battle game
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user