"""Deterministic AI command selection at three difficulty levels.""" from __future__ import annotations import random from typing import TYPE_CHECKING from python.gems.domain.engine import RuleError, apply_command, score from python.gems.domain.legal_actions import legal_commands if TYPE_CHECKING: from python.gems.domain.models import ContentPack, GameCommand, GameSettings, GameState def choose_ai_command( state: GameState, pack: ContentPack, settings: GameSettings, seat: int, difficulty: str, ) -> GameCommand: """Choose from legal commands using only public/current-player information.""" actions = legal_commands(state, pack, settings, seat) if not actions: error = "AI has no legal command" raise RuntimeError(error) rng = random.Random(f"{state.seed}:{state.revision}:{seat}:{difficulty}") # noqa: S311 - deterministic AI if difficulty == "easy": return rng.choice(actions) ranked = sorted(actions, key=lambda item: _heuristic(item, state, pack), reverse=True) if difficulty == "medium": return ranked[0] # Bounded deterministic rollout: examine at most the twelve strongest actions. best = ranked[0] best_value = float("-inf") for candidate in ranked[:12]: try: future = apply_command(state, candidate, pack, settings, actor_seat=seat) except RuleError: continue value = score(future.players[seat], pack) * 100 + _heuristic(candidate, state, pack) value += rng.random() * 0.001 if value > best_value: best, best_value = candidate, value return best def _heuristic(command: GameCommand, state: GameState, pack: ContentPack) -> float: player = state.players[state.current_seat] value = 0.0 if command.type == "purchase": card = pack.card(str(command.payload.get("card_id"))) value += 40 + card.points * 25 + (8 if card.bonus_resource else 0) value -= sum(command.payload.get("payment", {}).values()) elif command.type == "take_distinct": value += 10 + len(command.payload.get("resources", [])) elif command.type == "reserve": value += 6 elif command.type == "take_double": value += 4 elif command.type == "choose": value += 20 elif command.type == "decline": value -= 2 value += score(player, pack) * 0.01 return value