LovecaSim / engine /tests /conftest.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
import random
import pytest
from pytest_bdd import given, parsers, then, when
from compiler.parser import AbilityParser
from engine.game.data_loader import CardDataLoader
from engine.game.enums import Phase
from engine.game.game_state import GameState
from engine.models.ability import TriggerType
from engine.tests.framework.state_validators import StateValidator
# --- Common Fixtures ---
@pytest.fixture
def loader():
return CardDataLoader("engine/data/cards.json")
@pytest.fixture
def data(loader):
member_db, live_db, energy_pool = loader.load()
GameState.member_db = member_db
GameState.live_db = live_db
return member_db, live_db
@pytest.fixture
def context():
return {}
@pytest.fixture
def game_state(data, context):
gs = GameState()
# Setup simple decks
member_db, live_db = data
available_members = list(member_db.keys())
for p in gs.players:
if available_members:
# Create a simple deck
p.main_deck = [random.choice(available_members) for _ in range(20)]
p.hand = [] # Clear hand for controlled tests
gs.phase = Phase.MAIN
# Store in context for steps to access and update
context["game_state"] = gs
return gs
@pytest.fixture
def validated_game_state(data, context):
"""
A GameState fixture that runs StateValidator after every step.
Useful for ensuring deep integrity in new tests.
"""
class ValidatingGameState(GameState):
def step(self, action: int):
new_state = super().step(action)
# Validate the NEW state (which is a copy)
# But wait, step returns a NEW state object.
# We should probably validate 'new_state' if we want to catch issues there.
# AND validate 'self' if it was mutated (it is in some paths).
# Since GameState.step returns a COPY, 'new_state' is the one to check.
StateValidator.assert_valid(new_state)
return new_state
gs = ValidatingGameState()
# Identical setup to game_state fixture
member_db, live_db = data
available_members = list(member_db.keys())
for p in gs.players:
if available_members:
p.main_deck = [random.choice(available_members) for _ in range(20)]
p.hand = []
gs.phase = Phase.MAIN
context["game_state"] = gs
return gs
# --- Common Steps ---
@given("a player with a deck", target_fixture="player_state")
def player_with_deck(context, game_state):
# game_state fixture ensures context is populated
p = game_state.players[0]
context["initial_hand_size"] = len(p.hand)
context["initial_deck_size"] = len(p.main_deck)
return p
# --- Parser Steps (Generic) ---
@when(parsers.parse('I parse the ability text "{text}"'), target_fixture="parsed_abilities")
def parse_ability(text):
return AbilityParser.parse_ability_text(text)
@then(parsers.parse('I should get an ability with trigger "{trigger}"'))
def check_trigger(parsed_abilities, trigger):
assert len(parsed_abilities) > 0
# Map string to enum if needed, or check name
expected_trigger = getattr(TriggerType, trigger)
assert parsed_abilities[0].trigger == expected_trigger
@then(parsers.parse('the ability should have an effect type "{effect}"'))
def check_effect(parsed_abilities, effect):
if effect == "NONE":
assert len(parsed_abilities[0].effects) == 0
return
found = False
for abi in parsed_abilities:
for eff in abi.effects:
if eff.effect_type.name == effect:
found = True
break
assert found, f"Effect {effect} not found in {parsed_abilities[0].effects}"
@when(parsers.parse("the player draws {count:d} cards"))
def draw_cards(context, player_state, count):
# Note: _draw_cards is in-place
game_state = context["game_state"]
# We must ensure player_state is from the current game_state
# If player_state is passed, it is the initial state object usually.
# But usually draw_cards just acts on P0.
p = game_state.players[player_state.player_id]
game_state._draw_cards(p, count)
@then(parsers.parse("the player's hand size should increase by {count:d}"))
def check_hand_size(context, count):
game_state = context["game_state"]
player_state = game_state.players[0]
expected = context["initial_hand_size"] + count
assert len(player_state.hand) == expected, f"Expected {expected}, got {len(player_state.hand)}"
@then(parsers.parse("the player's deck size should decrease by {count:d}"))
def check_deck_size(context, count):
game_state = context["game_state"]
player_state = game_state.players[0]
expected = context["initial_deck_size"] - count
assert len(player_state.main_deck) == expected, f"Expected {expected}, got {len(player_state.main_deck)}"
@given(parsers.parse("the player has {count:d} cards in hand"))
def player_has_cards(context, game_state, count):
p = game_state.players[0]
# Use simple integers 100+
p.hand = list(range(100, 100 + count))
context["hand_cards"] = p.hand.copy()
@then(parsers.parse("the player should be prompted to select {count:d} card from hand"))
def check_prompt_select_hand(context, game_state, count):
current_state = context.get("game_state", game_state)
assert len(current_state.pending_choices) > 0
choice = current_state.pending_choices[0]
assert choice[0] == "TARGET_HAND"
# Action for TARGET_HAND 0 is 500.