Spaces:
Sleeping
Sleeping
| import numpy as np | |
| import pytest | |
| from engine.game.enums import Phase | |
| from engine.game.game_state import GameState | |
| from engine.models.card import MemberCard | |
| # Mock the specific card if it doesn't exist in the loaded DB (unlikely if data is good, but safe) | |
| # PL!-bp3-009-P+ (Nico Yazawa) | |
| # Cost 2 | |
| # [On Entry] If you have a member with Cost 13 or more, Draw 1. | |
| # [Action][Turn 1] Tap: Choose Heart (Pink/Yellow/Purple). Gain 1 until end of live. | |
| CARD_ID_NICO = "PL!-bp3-009-P+" | |
| # Note: Using the full-width plus as discovered | |
| def game_state(): | |
| gs = GameState() | |
| # Ensure we have the card in DB or mock it | |
| # We will try to find it in the real DB first | |
| found_id = None | |
| if gs.member_db: | |
| # Search by card_no | |
| for cid, c in gs.member_db.items(): | |
| if c.card_no == CARD_ID_NICO: | |
| found_id = cid | |
| break | |
| if found_id is None: | |
| # Inject mock if not found (fallback logic) | |
| mock_id = 99009 | |
| gs.member_db[mock_id] = MemberCard( | |
| card_id=mock_id, | |
| card_no=CARD_ID_NICO, | |
| name="Nico Yazawa", | |
| cost=2, | |
| blades=2, | |
| hearts=[0, 0, 0, 0, 0, 0, 1], # Dummy | |
| blade_hearts=[0, 0, 0, 0, 0, 0], | |
| # Use raw ability text to test Parser, OR pre-parsed if testing Engine only. | |
| # "【登場】自分のステージにコスト13以上のメンバーがいる場合、カードを1枚引く。\n【起動】【ターン1回】このメンバーをウェイトにする:{{heart_01.png}}か{{heart_03.png}}か{{heart_06.png}}のうち、1つを選ぶ。ライブ終了時まで、選んだハートを1つ得る。" | |
| ability_text="【登場】自分のステージにコスト13以上のメンバーがいる場合、カードを1枚引く。\n【起動】【ターン1回】このメンバーをウェイトにする:ハートのピンクか黄色か紫のうち、1つを選ぶ。ライブ終了時まで、選んだハートを1つ得る。", | |
| ) | |
| # Parse abilities for the mock | |
| from compiler.parser import AbilityParser | |
| parsed_abilities = AbilityParser.parse_ability_text(gs.member_db[mock_id].ability_text) | |
| gs.member_db[mock_id].abilities = parsed_abilities | |
| found_id = mock_id | |
| # Also need a Cost 13 Dummy | |
| mock_13_id = 99013 | |
| gs.member_db[mock_13_id] = MemberCard( | |
| card_id=mock_13_id, | |
| card_no="DUMMY-13", | |
| name="Big Cost", | |
| cost=13, | |
| blades=1, | |
| hearts=[1, 0, 0, 0, 0, 0, 0], | |
| blade_hearts=[0, 0, 0, 0, 0, 0], | |
| ) | |
| return gs, found_id, mock_13_id | |
| def test_nico_on_entry_no_condition(game_state): | |
| gs, nico_id, big_id = game_state | |
| p = gs.players[0] | |
| # Setup: Nico in hand, NO big member on stage | |
| gs.inject_card(0, nico_id, "hand") | |
| p.energy_zone = [200, 201, 202] # 3 Energy | |
| # Clean stage | |
| p.stage = [-1, -1, -1] | |
| p.tapped_members.fill(False) | |
| # Play Nico manually to trigger On Entry | |
| # Move card to stage 0 | |
| card_id = p.hand.pop(0) | |
| p.stage[0] = card_id | |
| # Manually trigger On Entry resolution | |
| # Normally handle_play_member would do this. | |
| # We can inject Pending Effects if we want to test Engine resolution, | |
| # OR we can assume Parser works and just test that the Condition holds/fails. | |
| def test_nico_on_entry_no_condition(game_state): | |
| gs, nico_id, big_id = game_state | |
| # Nico in hand | |
| gs.inject_card(0, nico_id, "hand", position=0) | |
| # 5 Energy (to pay cost 2) | |
| gs.players[0].tapped_energy = np.zeros(100, dtype=bool) | |
| gs.players[0].energy_zone = [100, 101, 102, 103, 104] | |
| # Phase MAIN | |
| gs.phase = Phase.MAIN | |
| gs.current_player = 0 | |
| initial_hand_size = len(gs.players[0].hand) | |
| # Play Nico (Action 1: hand 0 to slot 0) | |
| gs = gs.step(1) | |
| # Condition: Cost 13+ member on stage. | |
| # Stage has: [Nico(2), -1, -1]. Condition fails. | |
| # Hand after play: 0. | |
| assert len(gs.players[0].hand) == 0 | |
| assert gs.players[0].stage[0] == nico_id | |
| def test_nico_on_entry_with_condition(game_state): | |
| gs, nico_id, big_id = game_state | |
| # Setup condition: Big member on slot 1 | |
| gs.players[0].stage[1] = big_id | |
| # Nico in hand | |
| gs.inject_card(0, nico_id, "hand", position=0) | |
| # 5 Energy | |
| gs.players[0].tapped_energy = np.zeros(100, dtype=bool) | |
| gs.players[0].energy_zone = [100, 101, 102, 103, 104] | |
| # Deck has cards | |
| gs.players[0].main_deck = [300, 301, 302] | |
| # Phase MAIN | |
| gs.phase = Phase.MAIN | |
| gs.current_player = 0 | |
| initial_hand_size = len(gs.players[0].hand) | |
| # Play Nico (Action 1) | |
| gs = gs.step(1) | |
| # Condition: Cost 13+ member on stage. | |
| # Stage has: [Nico(2), Big(13), -1]. Condition succeeds. | |
| # Nico played (-1 hand), Draw 1 (+1 hand). Net change 0 (Nico was 1, then 0, then 1). | |
| assert len(gs.players[0].hand) == initial_hand_size | |
| assert gs.players[0].stage[0] == nico_id | |
| def test_nico_action_ability(game_state): | |
| gs, nico_id, big_id = game_state | |
| # Inject Nico to stage 0 | |
| gs.inject_card(0, nico_id, "stage", position=0) | |
| # Manual Ability Construction for Test Verification (since Parser/Engine missing COLOR_SELECT support) | |
| from engine.models.ability import ( | |
| Ability, | |
| AbilityCostType, | |
| Condition, | |
| ConditionType, | |
| Cost, | |
| Effect, | |
| EffectType, | |
| TargetType, | |
| TriggerType, | |
| ) | |
| # Ability 2: Action - Choose Heart | |
| ability_action = Ability( | |
| raw_text="Manually Constructed Ability", | |
| trigger=TriggerType.ACTIVATED, | |
| costs=[Cost(type=AbilityCostType.TAP_SELF, value=1)], | |
| conditions=[Condition(ConditionType.TURN_1)], | |
| effects=[ | |
| Effect( | |
| EffectType.COLOR_SELECT, | |
| value=1, | |
| target=TargetType.PLAYER, | |
| params={"choices": ["pink", "yellow", "purple"]}, | |
| ), | |
| Effect( | |
| EffectType.ADD_HEARTS, | |
| value=1, | |
| target=TargetType.MEMBER_SELF, | |
| params={ | |
| "color": "choice", # Special value indicating use previous choice | |
| "until": "live_end", | |
| }, | |
| ), | |
| ], | |
| ) | |
| # Needs to be Ability 2 (Index 1) in the list (On Entry is Index 0) | |
| # Re-fetch parsed abilities to get the first one | |
| from compiler.parser import AbilityParser | |
| parsed_abilities = AbilityParser.parse_ability_text(gs.member_db[nico_id].ability_text) | |
| # Override abilities | |
| gs.member_db[nico_id].abilities = [parsed_abilities[0], ability_action] | |
| # Set phase to MAIN_ACTION | |
| gs.phase = Phase.MAIN | |
| gs.current_player = 0 | |
| gs.players[0].tapped_members[0] = False # Ensure Active | |
| gs.action_count_this_turn = 0 | |
| gs.verbose = True # Enable verbose logging | |
| # Init JIT | |
| GameState._init_jit_arrays() | |
| # Debug: Print abilities | |
| print(f"\nDEBUG: Abilities on Nico: {gs.member_db[nico_id].abilities}") | |
| # Check legal actions | |
| legal = gs.get_legal_actions() | |
| print(f"DEBUG: Is Action 200 Legal? {legal[200]}") | |
| if not legal[200]: | |
| pytest.fail("Action 200 is not legal! Check parser or conditions.") | |
| # Action ID for activating ability of Member 0 is 200 | |
| # Ref: game_state.py:1020 -> mask[200 + i] = True | |
| # Executing action 200 | |
| gs = gs.step(200) | |
| # Should trigger Choice (Pink/Yellow/Purple) | |
| assert gs.pending_choices, "Should have pending choice for Heart select" | |
| choice_type, params = gs.pending_choices[0] | |
| assert choice_type == "COLOR_SELECT", f"Expected COLOR_SELECT, got {choice_type}" | |
| # Select Pink (Action 580) | |
| # Ref: game_state.py:1335 -> 580-585 are colors | |
| # Pink=0, index is 0. 580 + 0 = 580. | |
| gs.verbose = True | |
| gs = gs.step(580) | |
| # Verify buff | |
| # Slot 0 should have extra hearts. | |
| eff_hearts = gs.players[0].get_effective_hearts(0, gs.member_db) | |
| # Pink is index 0 | |
| # Base is Nico (Dummy) -> [0,0,0,0,0,0,1] | |
| # With buff -> Pink >= 1 | |
| assert eff_hearts[0] >= 1, f"Should have gained Pink heart. Got: {eff_hearts}" | |
| # Verify Turn 1 limit | |
| # Try using ability again | |
| # Should be illegal because it is Turn 1 | |
| # Check legal actions | |
| legal = gs.get_legal_actions() | |
| assert not legal[200], "Action should be illegal (Turn 1 limit or Tapped)" | |
| # Also verify it is tapped (Cost) | |
| assert gs.players[0].tapped_members[0], "Member should be tapped" | |