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)