LovecaSim / engine /tests /steps /test_ability_steps.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
import numpy as np
import pytest
from pytest_bdd import given, parsers, scenario, then, when
from engine.game.game_state import Group
from engine.models.ability import Effect, EffectType
# Import generic steps and fixtures
@scenario("../features/abilities.feature", "Draw cards")
def test_draw_cards():
pass
@scenario("../features/abilities.feature", "Add Blades")
def test_add_blades():
pass
@scenario("../features/abilities.feature", "Search Deck")
def test_search_deck():
pass
@scenario("../features/abilities.feature", "Recover member from discard")
def test_recover_member():
pass
@scenario("../features/abilities.feature", "Parse ability text")
def test_parse_ability():
pass
@scenario("../features/abilities.feature", "Formation Change")
def test_formation_change():
pass
# --- Steps ---
@given("a member card with blades", target_fixture="blade_member")
def member_with_blades(data):
member_db, _ = data
for m in member_db.values():
if m.blades > 0:
return m
@then("the member should have greater than 0 blades")
def check_blades(blade_member):
assert blade_member.blades > 0
@given(parsers.parse('a player with a deck containing "{group}" members'), target_fixture="player_with_search_target")
def player_with_search_target(context, game_state, data, group):
# game_state fixture ensures initialization
p = game_state.players[0]
member_db, _ = data
# Ensure deck has target
target_group = Group.from_japanese_name(group)
targets = [mid for mid, m in member_db.items() if target_group in m.groups]
if not targets:
pytest.skip(f"No members in group {group} found")
# Put targets at the top of the deck for easy searching, followed by other members
# Clear and update MemberDB to ensure we have the objects (handled by data fixture but safe to ensure)
p.main_deck = targets + [mid for mid in member_db if mid not in targets]
# Store context for later
return p
@when(parsers.parse('the player searches the deck for "{group}"'))
def search_deck(context, player_with_search_target, group):
game_state = context["game_state"]
# Simulate the effect: SEARCH_DECK
effect = Effect(EffectType.SEARCH_DECK, 1, params={"group": group})
game_state.pending_effects.append(effect)
# Resolve step -> Updates state!
game_state = game_state.step(0)
context["game_state"] = game_state
# Check if we have a choice
assert len(game_state.pending_choices) > 0
choice = game_state.pending_choices[0]
assert choice[0] == "SELECT_FROM_LIST"
# Select the first card found
cards_found = choice[1]["cards"]
assert len(cards_found) > 0
# Select it (Action 600)
game_state = game_state.step(600)
context["game_state"] = game_state
@then(parsers.parse('the player should find "{group}" members'))
def check_search_result(context, data, group):
game_state = context["game_state"]
p = game_state.players[0]
member_db, _ = data
target_group = Group.from_japanese_name(group)
# The player should have the card in hand now (since we selected it)
found_in_hand = [mid for mid in p.hand if mid in member_db and target_group in member_db[mid].groups]
assert len(found_in_hand) > 0
# --- Recover Member Steps ---
@given("a player has a member in discard", target_fixture="player_with_discard")
def player_with_discard(context, game_state, data):
# game_state ensures init
p = game_state.players[0]
member_db, _ = data
if not member_db:
pytest.skip("No members in DB")
# Add a member to discard
mid = list(member_db.keys())[0]
p.discard.append(mid)
context["discard_mid"] = mid
return p
@when("the player recovers the member from discard")
def recover_member(context):
game_state = context["game_state"]
p = game_state.players[0]
mid = context["discard_mid"]
# Simulate effect
if mid in p.discard:
p.discard.remove(mid)
p.hand.append(mid)
@then("the member should be in the player's hand")
def check_in_hand(context):
game_state = context["game_state"]
p = game_state.players[0]
assert context["discard_mid"] in p.hand
@then("the member should not be in the player's discard")
def check_not_in_discard(context):
game_state = context["game_state"]
p = game_state.players[0]
assert context["discard_mid"] not in p.discard
# --- Formation Change Steps ---
@given("a player with members on stage", target_fixture="player_with_stage")
def player_with_stage(context, game_state, data):
# game_state ensures init
p = game_state.players[0]
member_db, _ = data
# Setup stage: Slot 0 and Slot 1
# We need valid member IDs
ids = list(member_db.keys())
if len(ids) < 2:
pass
p.stage = np.full(3, -1, dtype=np.int32)
p.stage[0] = ids[0]
p.stage[1] = ids[1]
# Ensure they are "played" or at least valid
# Debug
# print(f"DEBUG: Stage set to {p.stage}")
return p
@when("the player activates formation change")
def activate_formation_change(context, player_with_stage):
game_state = context["game_state"]
effect = Effect(EffectType.FORMATION_CHANGE, 1)
game_state.pending_effects.append(effect)
# Resolve -> Select Slot 0
# Step returns NEW STATE
game_state = game_state.step(0)
context["game_state"] = game_state
assert len(game_state.pending_choices) > 0
assert game_state.pending_choices[0][0] == "SELECT_FORMATION_SLOT"
@when("the player selects to swap member at slot 0 with slot 1")
def select_swap(context, player_with_stage):
game_state = context["game_state"]
# 1. Select member for Slot 0 (Target: ID_B which is at index 1 of available)
# Available items logic in game_state: members list [(0, ID_A), (1, ID_B)]
# We assume standard ordering.
# Action 700 + index.
# Check choices to be sure?
# choices = game_state.pending_choices[0][1]["available_members"]
# We want to pick the one that was originally at slot 1.
# Just select index 1 (Action 701)
game_state = game_state.step(701)
context["game_state"] = game_state
# 2. Select member for Slot 1 (Target: ID_A which is at index 0 of remaining)
# Remaining: [(0, ID_A)]
# Action 700.
game_state = game_state.step(700)
context["game_state"] = game_state
@then("the members at slot 0 and 1 should be swapped")
def check_swap(context, player_with_stage):
game_state = context["game_state"]
p = game_state.players[0]
# We rely on checking that the IDs are different from what we set?
# Or capture original IDs in context?
# player_with_stage is the *fixture* return, which might be stale or not.
# Actually, player_with_stage fixture returned `p` (reference).
# Since `game_state` was COPIED, `player_with_stage` fixture refers to the OLD player object!
# We must access `game_state.players[0]`.
# We know simple Swap:
# Orig: [ID0, ID1, -1]
# New: [ID1, ID0, -1]
# We don't have easy access to ID0/ID1 values here unless we re-fetch from DB or context.
# But checking p.stage[0] != p.stage[1] is a sanity check.
# Checking against *original* is better.
# player_with_stage is the OLD player object.
old_stage = player_with_stage.stage
new_stage = p.stage
# Assert swapped
assert new_stage[0] == old_stage[1]
assert new_stage[1] == old_stage[0]