LovecaSim / compiler /tests /test_pseudocode_parsing.py
trioskosmos's picture
Upload folder using huggingface_hub
2113a6a verified
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