LovecaSim / engine /tests /cards /batches /test_all_abilities.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
"""
Automated smoke tests for all card abilities.
Uses the ability_test_generator to create parametrized tests for every card.
"""
import pytest
from engine.game.enums import Phase
from engine.models.ability import TriggerType
from engine.tests.framework.ability_test_generator import generate_ability_test_cases
from engine.tests.framework.state_validators import StateValidator
@pytest.mark.parametrize("test_case", generate_ability_test_cases())
def test_ability_executes_without_crash(game_state, test_case):
"""
Smoke test: Ensure every ability can be triggered and resolved without crashing.
Does NOT verify logical correctness of the effect, just that it doesn't crash the engine.
"""
# 1. Setup based on trigger type
trigger_name = test_case["trigger"]
cid = test_case["card_id"]
p = game_state.players[0]
# Skip unknown triggers for now
if trigger_name == "NONE" or trigger_name == 0:
pytest.skip("Skipping ability with no trigger")
try:
if isinstance(trigger_name, int):
trigger_enum = TriggerType(trigger_name)
trigger_name = trigger_enum.name
else:
trigger_enum = getattr(TriggerType, trigger_name)
except (AttributeError, ValueError):
# Might be integer in compiled JSON if not re-serialized with names
# Generator currently returns raw values if using names?
# Let's handle if it returns int mapped strings
pytest.skip(f"Skipping unknown trigger: {trigger_name}")
# Might be integer in compiled JSON if not re-serialized with names
# Generator currently returns raw values if using names?
# Let's handle if it returns int mapped strings
pytest.skip(f"Skipping unknown trigger: {trigger_name}")
# Initialize state for specific triggers
if trigger_name == "ON_PLAY":
# Setup: Hand -> Stage
# Give card + dummies for discard costs
p.hand = [cid] + [999] * 5
# Sync hand_added_turn to avoid validator desync if card stays in hand
# We need to sync for ALL cards
p.hand_added_turn = [0] * 6
# Give infinite energy for costs
p.energy_zone = [2000] * 10
p.tapped_energy[:] = False
game_state.phase = Phase.MAIN
# Execute: Play card
# Action 1 = Play card at index 0 to stage slot 0 (0*3 + 0 + 1 => 1)
game_state = game_state.step(1)
elif trigger_name == "ON_LIVE_START" or trigger_name == "LIVE_START":
# Setup: Card on Stage -> Live Start
p.stage[0] = cid
p.live_zone = [1000] # Dummy live
p.live_zone_revealed = [True]
game_state.phase = Phase.PERFORMANCE_P1
game_state.first_player = 0
game_state.current_player = 0
# Execute: Step(0) to trigger performance abilities
game_state = game_state.step(0)
elif trigger_name == "ACTIVATED":
# Setup: Card on Stage -> Activate
p.stage[0] = cid
p.tapped_members[0] = False
p.energy_zone = [2000] * 5 # Sufficient energy
p.tapped_energy[:] = False
game_state.phase = Phase.MAIN
# Execute: Activate ability (Action 200 = Activate Slot 0)
game_state = game_state.step(200)
elif trigger_name == "CONSTANT":
# Setup: Card on Stage
p.stage[0] = cid
# Constant effects are automatic, just check state or continue
game_state.phase = Phase.MAIN
elif trigger_name == "TURN_END":
# Setup: On Stage -> End Turn
p.stage[0] = cid
game_state.phase = Phase.MAIN
# Execute: End turn (Action 0 in Main Phase)
game_state = game_state.step(0)
elif trigger_name == "TURN_START":
# Setup: On Stage -> Start Turn
p.stage[0] = cid
game_state.phase = Phase.DRAW
# Execute: Process draw phase to reach turn start logic (usually handled by process_rule_checks or start_turn)
game_state = game_state.step(0)
elif trigger_name == "ON_REVEAL":
# Setup: Manually queue the ability to simulate trigger
target_ability = None
if cid in game_state.member_db:
card = game_state.member_db[cid]
for ab in card.abilities:
if ab.trigger == TriggerType.ON_REVEAL:
target_ability = ab
break
elif cid in game_state.live_db:
card = game_state.live_db[cid]
for ab in card.abilities:
if ab.trigger == TriggerType.ON_REVEAL:
target_ability = ab
break
if not target_ability:
from engine.models.ability import Ability, Effect, EffectType, TargetType
target_ability = Ability(
raw_text="Mock Reveal",
trigger=TriggerType.ON_REVEAL,
effects=[Effect(EffectType.DRAW, 1, TargetType.PLAYER)],
conditions=[],
costs=[],
)
if target_ability:
game_state.triggered_abilities.append((0, target_ability, {"card_id": cid, "zone": "DECK"}))
game_state.looked_cards = [cid] # Mock usage
game_state.phase = Phase.PERFORMANCE_P1
game_state = game_state.step(0)
else:
pytest.skip("Could not find ON_REVEAL ability on card")
elif trigger_name == "ON_LEAVES":
# Setup: Card on Stage -> Manually queue ability
p.stage[0] = cid
target_ability = None
if cid in game_state.member_db:
card = game_state.member_db[cid]
for ab in card.abilities:
if ab.trigger == TriggerType.ON_LEAVES:
target_ability = ab
break
if not target_ability:
from engine.models.ability import Ability, Effect, EffectType, TargetType
target_ability = Ability(
raw_text="Mock Leaves",
trigger=TriggerType.ON_LEAVES,
effects=[Effect(EffectType.DRAW, 1, TargetType.PLAYER)],
conditions=[],
costs=[],
)
if target_ability:
# Simulate leaving (add to discard, remove from stage)
p.stage[0] = -1
p.discard.append(cid)
p.moved_members_this_turn.add(cid) # Satisfy HAS_MOVED condition if present
game_state.triggered_abilities.append(
(0, target_ability, {"card_id": cid, "area": 0, "from_zone": "STAGE"})
)
game_state.phase = Phase.MAIN
game_state = game_state.step(0)
else:
pytest.skip("Could not find ON_LEAVES ability on card")
elif trigger_name == "ON_LIVE_SUCCESS":
# Setup: Live success check
# We need a live in live zone, and a stage that meets requirements.
p.live_zone = [2000] # Use a known live or dummy
p.live_zone_revealed = [True]
# Mock requirements to be met
# We can't easily force success without exact hearts.
# But we can try to Phase.LIVE_RESULT
game_state.phase = Phase.LIVE_RESULT
# This might be enough to trigger 'ON_LIVE_SUCCESS' if the engine processing flow hits it.
game_state = game_state.step(0)
else:
# Other triggers (ON_REVEAL, etc.) might be passive or handled differently
# For now, we skip complex setups to keep smoke test fast
pytest.skip(f"Trigger {trigger_name} setup not implemented yet")
# 2. Resolve any resulting choices (dumb AI approach)
# We loop up to 10 times to resolve chained checks/choices
for _ in range(10):
# Continue if there are choices OR pending triggers/effects
if not game_state.pending_choices and not game_state.triggered_abilities and not game_state.pending_effects:
break
# Make the first available choice
legal = game_state.get_legal_actions()
action = 0
# Prefer non-pass actions if available, unless optional
import numpy as np
legal_indices = np.where(legal)[0]
if len(legal_indices) > 0:
# Pick first valid action (prefer >0)
valid_actions = [x for x in legal_indices if x > 0]
if valid_actions:
action = valid_actions[0]
else:
action = 0 # Pass/Decline
game_state = game_state.step(action)
# Cleanup mock state for ON_REVEAL to prevent STALE_LOOKED_CARDS
# Cleanup transient state that might remain if engine logic didn't fully clear (e.g. paused abilities)
game_state.looked_cards = []
# Cleanup live_zone_revealed to match live_zone to prevent LIVE_ZONE_DESYNC
for p_idx in range(2):
player = game_state.players[p_idx]
if len(player.live_zone_revealed) != len(player.live_zone):
player.live_zone_revealed = [True] * len(player.live_zone)
# 3. Assert Valid State
errors = StateValidator.validate_post_step(game_state)
assert not errors, f"State validation failed for card {cid} ({test_case['card_name']}): {errors}"