treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 2m25s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-rhapsody-in-green (push) Successful in 49s
- implement FastAPI and HTMX lobby and game interfaces - support up to four human and AI-controlled players - add configurable rules, victory conditions, and expansion modules - support validated JSON cards, patrons, objectives, and outposts
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
"""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
|