File size: 5,551 Bytes
2113a6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import pytest

from compiler.parser_v2 import AbilityParserV2
from engine.models.ability import AbilityCostType, ConditionType, EffectType, TargetType, TriggerType


@pytest.fixture
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