Files
dotfiles/tests/gems/test_base_rules.py
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

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