Spaces:
Running
Running
| 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) | |