Files
dotfiles/python/gems/ai/runner.py
T
Richie add7a6a848
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
feat(gems): add multiplayer gem game with custom content packs
- 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
2026-07-22 20:47:03 -04:00

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