161 lines
6.8 KiB
Python
161 lines
6.8 KiB
Python
"""AI player: simple rule-based decision making."""
|
|
|
|
from card_game.config import CARD_DATABASE
|
|
|
|
|
|
class AIPlayer:
|
|
def __init__(self, battlefield):
|
|
self.battlefield = battlefield
|
|
|
|
def execute_turn(self):
|
|
actions = []
|
|
ai = self.battlefield.ai
|
|
|
|
actions.extend(self._play_untargeted_orders(ai))
|
|
actions.extend(self._deploy_units(ai))
|
|
actions.extend(self._play_targeted_orders(ai))
|
|
actions.extend(self._move_units(ai))
|
|
actions.extend(self._attack(ai))
|
|
|
|
return actions
|
|
|
|
def _play_untargeted_orders(self, ai):
|
|
actions = []
|
|
for card in ai.hand[:]:
|
|
if card.card_type != "order":
|
|
continue
|
|
if card.cost > ai.provisions:
|
|
continue
|
|
if not self.battlefield.needs_target(card):
|
|
self.battlefield.apply_order_effect(card, ai)
|
|
ai.play_order(card)
|
|
actions.append(("order", card))
|
|
return actions
|
|
|
|
def _deploy_units(self, ai):
|
|
actions = []
|
|
units = [c for c in ai.hand if c.card_type == "unit" and ai.can_play_card(c)]
|
|
units.sort(key=lambda c: c.cost)
|
|
for card in units:
|
|
if not ai.can_play_card(card):
|
|
continue
|
|
from card_game.factions import apply_faction_passive
|
|
apply_faction_passive(card, ai.faction_id)
|
|
slot = ai.deploy_unit(card)
|
|
if slot >= 0:
|
|
actions.append(("deploy", card, slot))
|
|
self._handle_deploy(card, ai)
|
|
return actions
|
|
|
|
def _play_targeted_orders(self, ai):
|
|
actions = []
|
|
for card in ai.hand[:]:
|
|
if card.card_type != "order":
|
|
continue
|
|
if card.cost > ai.provisions:
|
|
continue
|
|
if not self.battlefield.needs_target(card):
|
|
continue
|
|
targets = self.battlefield.get_valid_targets(card, ai)
|
|
if targets:
|
|
target = self._pick_best_target(card, targets, ai)
|
|
if target:
|
|
self.battlefield.apply_order_effect(card, ai, target)
|
|
ai.play_order(card)
|
|
actions.append(("order_targeted", card, target))
|
|
return actions
|
|
|
|
def _move_units(self, ai):
|
|
actions = []
|
|
if not self.battlefield.can_move_to_frontline(ai):
|
|
return actions
|
|
for unit in ai.get_support_units():
|
|
op_cost = ai._get_op_cost(unit)
|
|
if ai.provisions >= op_cost:
|
|
slot = ai.move_to_frontline(unit)
|
|
if slot >= 0:
|
|
if self.battlefield.frontline_controller is None:
|
|
self.battlefield.claim_frontline(ai)
|
|
actions.append(("move", unit, slot))
|
|
return actions
|
|
|
|
def _attack(self, ai):
|
|
actions = []
|
|
player = self.battlefield.player
|
|
|
|
# Frontline units: attack enemy frontline > enemy support > capital
|
|
for unit in ai.get_frontline_units():
|
|
if not unit.can_attack or unit.has_attacked or not ai.can_afford_attack(unit):
|
|
continue
|
|
enemy_front = player.get_frontline_units()
|
|
enemy_support = player.get_support_units()
|
|
all_enemy = enemy_front + enemy_support
|
|
if all_enemy:
|
|
killable = [u for u in all_enemy if u.current_hp <= unit.get_effective_attack()]
|
|
if killable:
|
|
target = min(killable, key=lambda u: u.current_hp)
|
|
else:
|
|
target = max(all_enemy, key=lambda u: u.get_effective_attack())
|
|
self.battlefield.resolve_attack(unit, target)
|
|
actions.append(("attack_unit", unit, target))
|
|
else:
|
|
self.battlefield.attack_capital(unit)
|
|
actions.append(("attack_capital", unit))
|
|
|
|
# Support units: attack enemy frontline > enemy support > capital (ranged only)
|
|
for unit in ai.get_support_units():
|
|
if not unit.can_attack or unit.has_attacked or not ai.can_afford_attack(unit):
|
|
continue
|
|
enemy_front = player.get_frontline_units()
|
|
enemy_support = player.get_support_units()
|
|
if unit.is_ranged():
|
|
all_enemy = enemy_front + enemy_support
|
|
if all_enemy:
|
|
killable = [u for u in all_enemy if u.current_hp <= unit.get_effective_attack()]
|
|
target = min(killable, key=lambda u: u.current_hp) if killable else min(all_enemy, key=lambda u: u.current_hp)
|
|
self.battlefield.resolve_attack(unit, target)
|
|
actions.append(("attack_unit", unit, target))
|
|
else:
|
|
self.battlefield.attack_capital(unit)
|
|
actions.append(("attack_capital", unit))
|
|
elif enemy_front:
|
|
killable = [u for u in enemy_front if u.current_hp <= unit.get_effective_attack()]
|
|
target = min(killable, key=lambda u: u.current_hp) if killable else min(enemy_front, key=lambda u: u.current_hp)
|
|
self.battlefield.resolve_attack(unit, target)
|
|
actions.append(("attack_unit", unit, target))
|
|
|
|
return actions
|
|
|
|
def _handle_deploy(self, card, ai):
|
|
for ability in card.abilities:
|
|
if ability.startswith("draw_on_deploy:"):
|
|
count = int(ability.split(":")[1])
|
|
for _ in range(count):
|
|
ai.draw_card()
|
|
elif ability.startswith("damage_on_deploy:"):
|
|
dmg = int(ability.split(":")[1])
|
|
opponent = self.battlefield.get_opponent(ai)
|
|
targets = opponent.get_all_units()
|
|
if targets:
|
|
import random
|
|
target = random.choice(targets)
|
|
target.take_damage(dmg)
|
|
|
|
def _pick_best_target(self, card, targets, ai):
|
|
if not targets:
|
|
return None
|
|
if card.effect_type == "destroy_damaged":
|
|
dmg_targets = [t for t in targets if hasattr(t, 'current_hp') and t.current_hp < t.max_hp]
|
|
if dmg_targets:
|
|
return min(dmg_targets, key=lambda u: u.current_hp)
|
|
return None
|
|
if card.effect_type == "bounce":
|
|
return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0)
|
|
if card.effect_type in ("buff_single", "move_to_front"):
|
|
return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0)
|
|
if card.effect_type == "damage":
|
|
unit_targets = [t for t in targets if hasattr(t, 'get_effective_attack')]
|
|
if unit_targets:
|
|
return max(unit_targets, key=lambda u: u.get_effective_attack())
|
|
return targets[0] if targets else None
|