LovecaSim / engine /tests /mechanics /test_atomic_discard.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
import os
import sys
# Add project root to path
sys.path.insert(0, os.getcwd())
import numpy as np
from engine.game.enums import Phase
from engine.game.game_state import GameState
from engine.models.ability import Ability, AbilityCostType, Cost, Effect, EffectType, TriggerType
from engine.models.card import MemberCard
def test_multi_discard_atomicity():
"""
Verify that for a 'Discard 2: Draw 1' ability, the 'Draw' effect
does not happen until BOTH discards are completed.
"""
game = GameState()
p1 = game.players[0]
# Ability: [Startup] Cost: Discard 2 -> Effect: Draw 1
ability = Ability(
raw_text="Discard 2: Draw 1",
trigger=TriggerType.ACTIVATED,
costs=[Cost(type=AbilityCostType.DISCARD_HAND, value=2)],
effects=[Effect(effect_type=EffectType.DRAW, value=1)],
)
card = MemberCard(
card_id=1,
card_no="TEST-001",
name="Test Card",
cost=1,
hearts=np.zeros(7, dtype=np.int32),
blade_hearts=np.zeros(7, dtype=np.int32),
blades=1,
abilities=[ability],
)
game.member_db[1] = card
# Setup: Hand has 3 cards.
# [100, 101, 102]
# We want to discard [100, 101] and draw something else.
p1.hand = [100, 101, 102]
p1.hand_added_turn = [0, 0, 0]
p1.stage[0] = 1 # Put test card on stage
# Deck has [103]
p1.main_deck = [103]
from engine.game.enums import Phase
game.phase = Phase.MAIN
# 1. Activate ability
game._execute_action(200) # Activate Slot 0
assert len(game.pending_choices) == 1, f"Expected 1 pending choice, got {len(game.pending_choices)}"
assert game.pending_choices[0][0] == "TARGET_HAND", f"Expected TARGET_HAND choice, got {game.pending_choices[0][0]}"
assert game.pending_choices[0][1]["count"] == 2, f"Expected count 2, got {game.pending_choices[0][1]['count']}"
# 2. Perform 1st discard (Choose index 0)
print(f"DEBUG: Before 1st discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
game._execute_action(500)
print(f"DEBUG: After 1st discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
# Check state:
# If it's correctly atomic:
# Hand should be [101, 102]
# Discard should be [100]
# pending_choices should have 1 item (the 2nd discard)
# DRAW should NOT have happened yet.
assert len(p1.hand) == 2, f"Hand size {len(p1.hand)} != 2 after 1st discard"
assert 103 not in p1.hand, "Should NOT have drawn 103 yet!"
assert len(game.pending_choices) == 1, f"Expected 1 choice, got {len(game.pending_choices)}"
assert game.pending_choices[0][1]["count"] == 1
# 3. Perform 2nd discard (Choose index 0 which is card 101)
print(f"DEBUG: Before 2nd discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
game._execute_action(500)
print(f"DEBUG: After 2nd discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
# Now DRAW should have happened.
assert 103 in p1.hand, "Should have drawn 103 now"
# Note: Discard might be empty if a refresh occurred (which it does in this test setup)
assert len(p1.hand) >= 2
def test_swap_cards_atomicity():
"""
Verify that for a 'SWAP_CARDS (2)' effect (Discard 2, then Draw 2),
draws do NOT happen until both discards are done.
"""
game = GameState()
p1 = game.players[0]
# Ability: [On Play] Effect: Swap 2
ability = Ability(
raw_text="On Play: Swap 2",
trigger=TriggerType.ON_PLAY,
costs=[],
effects=[Effect(effect_type=EffectType.SWAP_CARDS, value=2)],
)
card = MemberCard(
card_id=1,
card_no="TEST-002",
name="Swap Card",
cost=1,
hearts=np.zeros(7, dtype=np.int32),
blade_hearts=np.zeros(7, dtype=np.int32),
blades=1,
abilities=[ability],
)
game.member_db[1] = card
# Setup
p1.hand = [1, 100, 101, 102]
p1.hand_added_turn = [0, 0, 0, 0]
p1.main_deck = [200, 201]
game.phase = Phase.MAIN
# 1. Play card 1 (triggers ON_PLAY)
game._execute_action(1) # Play hand[0] (card 1) to slot 0
# Effect should be resolving.
# It should have queued a DISCARD_SELECT choice.
assert len(game.pending_choices) == 1
assert game.pending_choices[0][0] == "DISCARD_SELECT"
# 2. Perform 1st discard
print(f"DEBUG SWAP: Before 1st discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
game._execute_action(500)
print(f"DEBUG SWAP: After 1st discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
# If it's INTERLEAVING (Buggy):
# Hand will contain a card from the deck already!
assert 200 not in p1.hand, "Should NOT have drawn first card yet!"
assert len(game.pending_choices) == 1, "Should have 2nd discard pending"
# 3. Perform 2nd discard
game._execute_action(500)
print(f"DEBUG SWAP: After 2nd discard: Hand={p1.hand} Discard={p1.discard} Choices={game.pending_choices}")
# Now both should be drawn.
assert 200 in p1.hand
assert 201 in p1.hand
assert len(p1.hand) == 3
if __name__ == "__main__":
try:
print("=== RUNNING COST ATOMICITY TEST ===", flush=True)
test_multi_discard_atomicity()
print("=== COST TEST PASSED ===", flush=True)
print("\n=== RUNNING SWAP ATOMICITY TEST ===", flush=True)
test_swap_cards_atomicity()
print("=== SWAP TEST PASSED ===", flush=True)
except AssertionError as e:
print(f"\n!!! TEST FAILED: {e} !!!", flush=True)
sys.exit(1)
except Exception as e:
print(f"\n!!! ERROR: {e} !!!", flush=True)
import traceback
traceback.print_exc()
sys.exit(1)