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
118 lines
5.4 KiB
Python
118 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from python.gems.ai import choose_ai_command
|
|
from python.gems.domain.engine import RuleError, apply_command, bonuses, new_game, public_state, score
|
|
from python.gems.domain.legal_actions import command, legal_commands
|
|
from python.gems.domain.models import GameSettings, OwnedCard
|
|
|
|
|
|
def test_setup_and_hidden_information(content_pack) -> None:
|
|
state = new_game("ABCDEFGH", ["A", "B", "C", "D"], content_pack, GameSettings(), seed=9)
|
|
assert state.supply["pearl"] == 7
|
|
assert all(len(market) == 4 for market in state.markets.values())
|
|
state.players[1].reserved.append("base_1_0")
|
|
view = public_state(state, 0)
|
|
assert view["players"][1]["reserved"] == [None]
|
|
assert isinstance(view["decks"]["base:1"], int)
|
|
|
|
|
|
def test_distinct_take_requires_three_when_available(content_pack) -> None:
|
|
settings = GameSettings(first_player_mode="selected", first_player_seat=0)
|
|
state = new_game("ABCDEFGH", ["A", "B"], content_pack, settings, seed=1)
|
|
bad = command(state, "take_distinct", {"resources": ["pearl", "wave"]})
|
|
with pytest.raises(RuleError, match="exactly three"):
|
|
apply_command(state, bad, content_pack, settings, actor_seat=0)
|
|
good = command(state, "take_distinct", {"resources": ["pearl", "wave", "leaf"]})
|
|
state = apply_command(state, good, content_pack, settings, actor_seat=0)
|
|
assert state.players[0].tokens["pearl"] == 1
|
|
assert state.current_seat == 1
|
|
|
|
|
|
def test_purchase_uses_discount_and_returns_payment(content_pack) -> None:
|
|
settings = GameSettings(first_player_mode="selected", first_player_seat=0)
|
|
state = new_game("ABCDEFGH", ["A", "B"], content_pack, settings, seed=2)
|
|
card_id = state.markets["base:1"][0]
|
|
card = content_pack.card(card_id)
|
|
resource, cost = next(iter(card.cost.items()))
|
|
state.players[0].tokens[resource] = cost
|
|
state.supply[resource] -= cost
|
|
buy = command(state, "purchase", {"card_id": card_id, "payment": {resource: cost}})
|
|
state = apply_command(state, buy, content_pack, settings, actor_seat=0)
|
|
assert any(item.card_id == card_id for item in state.players[0].cards)
|
|
assert state.supply[resource] >= cost
|
|
assert bonuses(state.players[0], content_pack)[card.bonus_resource] == 1
|
|
|
|
|
|
def test_reserving_visible_card_refills_market(content_pack) -> None:
|
|
settings = GameSettings(first_player_mode="selected", first_player_seat=0)
|
|
state = new_game("ABCDEFGH", ["A", "B"], content_pack, settings, seed=12)
|
|
card_id = state.markets["base:1"][0]
|
|
deck_size = len(state.decks["base:1"])
|
|
|
|
state = apply_command(state, command(state, "reserve", {"card_id": card_id}), content_pack, settings, actor_seat=0)
|
|
|
|
assert card_id in state.players[0].reserved
|
|
assert card_id not in state.markets["base:1"]
|
|
assert len(state.markets["base:1"]) == settings.base_market_size
|
|
assert len(state.decks["base:1"]) == deck_size - 1
|
|
|
|
|
|
def test_score_counts_cards_patrons_and_scoring_outpost(content_pack) -> None:
|
|
state = new_game("ABCDEFGH", ["A"], content_pack, GameSettings(), seed=3)
|
|
player = state.players[0]
|
|
player.cards.append(OwnedCard(card_id="base_3_0"))
|
|
player.patrons.append("patron_one")
|
|
player.outposts.append("post_points_per_outpost")
|
|
assert score(player, content_pack) == 6
|
|
|
|
|
|
def test_each_ai_level_returns_a_legal_command(content_pack) -> None:
|
|
settings = GameSettings(first_player_mode="selected", first_player_seat=0)
|
|
state = new_game("ABCDEFGH", ["A", "B"], content_pack, settings, seed=4)
|
|
legal = legal_commands(state, content_pack, settings, 0)
|
|
legal_shapes = {(item.type, str(item.payload)) for item in legal}
|
|
for difficulty in ("easy", "medium", "hard"):
|
|
chosen = choose_ai_command(state, content_pack, settings, 0, difficulty)
|
|
assert (chosen.type, str(chosen.payload)) in legal_shapes
|
|
|
|
|
|
def test_first_player_can_be_selected_or_random(content_pack) -> None:
|
|
selected = GameSettings(first_player_mode="selected", first_player_seat=2)
|
|
selected_state = new_game("ABCDEFGH", ["A", "B", "C"], content_pack, selected, seed=8)
|
|
assert selected_state.first_seat == 2
|
|
assert selected_state.current_seat == 2
|
|
|
|
random_settings = GameSettings(first_player_mode="random")
|
|
first = new_game("ABCDEFGH", ["A", "B", "C"], content_pack, random_settings, seed=8)
|
|
repeated = new_game("ABCDEFGH", ["A", "B", "C"], content_pack, random_settings, seed=8)
|
|
assert first.first_seat == repeated.first_seat
|
|
assert 0 <= first.first_seat < 3
|
|
|
|
|
|
def test_excess_tokens_can_be_discarded_and_returned_to_supply(content_pack) -> None:
|
|
settings = GameSettings(token_limit=1, first_player_mode="selected", first_player_seat=0)
|
|
state = new_game("ABCDEFGH", ["A", "B"], content_pack, settings, seed=6)
|
|
colors = list(content_pack.resource_ids[:3])
|
|
|
|
state = apply_command(
|
|
state,
|
|
command(state, "take_distinct", {"resources": colors}),
|
|
content_pack,
|
|
settings,
|
|
actor_seat=0,
|
|
)
|
|
assert state.pending is not None
|
|
assert state.pending.kind == "discard_tokens"
|
|
assert state.pending.amount == 2
|
|
|
|
discard = legal_commands(state, content_pack, settings, 0)[0]
|
|
returned = sum(discard.payload["tokens"].values())
|
|
state = apply_command(state, discard, content_pack, settings, actor_seat=0)
|
|
|
|
assert returned == 2
|
|
assert sum(state.players[0].tokens.values()) == 1
|
|
assert state.pending is None
|
|
assert state.current_seat == 1
|