Spaces:
Running
Running
| import pytest | |
| from compiler.parser_v2 import AbilityParserV2 | |
| from engine.models.ability import AbilityCostType, ConditionType, EffectType, TargetType, TriggerType | |
| def parser(): | |
| return AbilityParserV2() | |
| def test_basic_trigger(parser): | |
| text = "TRIGGER: ON_PLAY\nEFFECT: DRAW(1) -> PLAYER" | |
| abilities = parser.parse(text) | |
| assert len(abilities) == 1 | |
| assert abilities[0].trigger == TriggerType.ON_PLAY | |
| assert abilities[0].effects[0].effect_type == EffectType.DRAW | |
| def test_once_per_turn(parser): | |
| text = "TRIGGER: ACTIVATED\n(Once per turn)\nEFFECT: DRAW(1) -> PLAYER" | |
| abilities = parser.parse(text) | |
| assert abilities[0].is_once_per_turn == True | |
| # Alternative format (same line) | |
| text2 = "TRIGGER: ACTIVATED (Once per turn)\nEFFECT: DRAW(1) -> PLAYER" | |
| abilities2 = parser.parse(text2) | |
| assert abilities2[0].is_once_per_turn == True | |
| def test_costs(parser): | |
| text = "TRIGGER: ACTIVATED\nCOST: ENERGY(2), TAP_SELF(0) (Optional)\nEFFECT: DRAW(1) -> PLAYER" | |
| abilities = parser.parse(text) | |
| costs = abilities[0].costs | |
| assert len(costs) == 2 | |
| assert costs[0].type == AbilityCostType.ENERGY | |
| assert costs[0].value == 2 | |
| assert costs[1].type == AbilityCostType.TAP_SELF | |
| assert costs[1].is_optional == True | |
| def test_conditions(parser): | |
| text = "TRIGGER: ON_PLAY\nCONDITION: COUNT_STAGE {MIN=3}, NOT HAS_COLOR {COLOR=1}\nEFFECT: DRAW(1) -> PLAYER" | |
| abilities = parser.parse(text) | |
| conds = abilities[0].conditions | |
| assert len(conds) == 2 | |
| assert conds[0].type == ConditionType.COUNT_STAGE | |
| assert conds[0].params["min"] == 3 | |
| assert conds[1].type == ConditionType.HAS_COLOR | |
| assert conds[1].params["color"] == 1 | |
| assert conds[1].is_negated == True | |
| def test_effects_with_params(parser): | |
| text = 'TRIGGER: ON_PLAY\nEFFECT: RECOVER_MEMBER(1) -> CARD_DISCARD {GROUP="μ\'s", FROM=DISCARD, TO=HAND}' | |
| abilities = parser.parse(text) | |
| eff = abilities[0].effects[0] | |
| assert eff.effect_type == EffectType.RECOVER_MEMBER | |
| assert eff.target == TargetType.CARD_DISCARD | |
| assert eff.params["group"] == "μ's" | |
| assert eff.params["from"] == "discard" | |
| assert eff.params["to"] == "hand" | |
| def test_case_normalization(parser): | |
| # Enums should be case-insensitive in parameters where it makes sense | |
| text = "TRIGGER: ON_LIVE_START\nEFFECT: BOOST_SCORE(3) -> PLAYER {UNTIL=LIVE_END}" | |
| abilities = parser.parse(text) | |
| assert abilities[0].effects[0].params["until"] == "live_end" | |
| def test_embedded_json_params(parser): | |
| # Some legacy/complex params might be stored as raw JSON in pseudocode | |
| text = 'TRIGGER: ON_PLAY\nEFFECT: LOOK_AND_CHOOSE(1) -> CARD_DISCARD {GROUP="Liella!", {"look_count": 5}}' | |
| abilities = parser.parse(text) | |
| params = abilities[0].effects[0].params | |
| assert params["group"] == "Liella!" | |
| assert params["look_count"] == 5 | |
| def test_multiple_abilities(parser): | |
| text = """TRIGGER: ON_PLAY | |
| EFFECT: DRAW(1) -> PLAYER | |
| TRIGGER: TURN_START | |
| EFFECT: ADD_BLADES(1) -> PLAYER""" | |
| abilities = parser.parse(text) | |
| assert len(abilities) == 2 | |
| assert abilities[0].trigger == TriggerType.ON_PLAY | |
| assert abilities[1].trigger == TriggerType.TURN_START | |
| def test_numeric_parsing(parser): | |
| # Ensure values and params handle numbers correctly | |
| text = "TRIGGER: ACTIVATED\nCOST: DISCARD_HAND(3)\nCONDITION: COUNT_ENERGY {MIN=5}\nEFFECT: ADD_HEARTS(2) -> PLAYER" | |
| abilities = parser.parse(text) | |
| ab = abilities[0] | |
| assert ab.costs[0].value == 3 | |
| assert ab.conditions[0].params["min"] == 5 | |
| assert ab.effects[0].value == 2 | |
| def test_optional_effect(parser): | |
| text = "TRIGGER: ON_PLAY\nEFFECT: SEARCH_DECK(1) -> CARD_HAND {FROM=DECK} (Optional)" | |
| abilities = parser.parse(text) | |
| assert abilities[0].effects[0].is_optional == True | |
| def test_robust_param_splitting(parser): | |
| # Mixed spaces and commas | |
| text = ( | |
| 'TRIGGER: ON_PLAY\nEFFECT: RECOVER_LIVE(1) -> CARD_DISCARD {GROUP="BiBi",GROUPS=["BiBi","BiBi"], FROM=DISCARD }' | |
| ) | |
| abilities = parser.parse(text) | |
| params = abilities[0].effects[0].params | |
| assert params["group"] == "BiBi" | |
| assert params["groups"] == ["BiBi", "BiBi"] | |
| assert params["from"] == "discard" | |
| def test_bytecode_compilation(parser): | |
| # This is the ultimate proof that the code "works" in the engine | |
| text = """TRIGGER: ON_LIVE_START | |
| COST: DISCARD_HAND(3) {NAMES=["上原歩夢", "澁谷かのん", "日野下花帆"]} (Optional) | |
| EFFECT: BOOST_SCORE(3) -> PLAYER {UNTIL=LIVE_END}""" | |
| abilities = parser.parse(text) | |
| ab = abilities[0] | |
| # Compile to bytecode | |
| bytecode = ab.compile() | |
| # Bytecode should: | |
| # 1. Be a list of integers | |
| # 2. Have length that is multiple of 4 | |
| # 3. End with the RETURN opcode (usually 0 in some engines, but let's check length) | |
| assert isinstance(bytecode, list) | |
| assert len(bytecode) > 0 | |
| assert len(bytecode) % 4 == 0 | |
| print(f"Compiled Bytecode: {bytecode}") | |
| def test_complex_condition_compilation(parser): | |
| text = 'TRIGGER: ON_PLAY\nCONDITION: COUNT_GROUP {GROUP="μ\'s", MIN=2}\nEFFECT: DRAW(1) -> PLAYER' | |
| abilities = parser.parse(text) | |
| ab = abilities[0] | |
| bytecode = ab.compile() | |
| assert len(bytecode) % 4 == 0 | |
| # First chunk should be a check for group count | |
| # Opcode.CHECK_COUNT_GROUP is 1000 + enum value or similar | |
| assert bytecode[0] > 0 | |