"""Parser + engine smoke tests. Run with: python -m tests.test_parser These use the mock backend, so no model weights or network are required.""" import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from engine.game_state import GameState, Enemy from engine import parser from engine.engine import GameEngine from engine.llm import build_backend def check(name, cond): status = "ok " if cond else "FAIL" print(f"[{status}] {name}") if not cond: raise AssertionError(name) def test_parse_blocks(): raw = ( "You enter a dim hall." "\nHP: -5\nGOLD: +10\nITEM_ADD: Torch\nLOCATION: Dim Hall\n" "\n1. Go north.\n2. Light the torch.\n" ) narrative, choices, lines = parser.parse(raw) check("narrative extracted", narrative == "You enter a dim hall.") check("choices extracted", choices == ["Go north.", "Light the torch."]) check("state lines count", len(lines) == 4) def test_apply_changes_clamped(): state = GameState(hp=20, max_hp=20, gold=10) parser.apply_state_changes(state, ["HP: -5", "GOLD: +10", "ITEM_ADD: Torch"]) check("hp reduced", state.hp == 15) check("gold added", state.gold == 20) check("item added", state.has_item("Torch")) # over-heal is clamped to max_hp parser.apply_state_changes(state, ["HP: +999"]) check("heal clamped to max", state.hp == 20) # can't go below zero gold parser.apply_state_changes(state, ["GOLD: -9999"]) check("gold floored at 0", state.gold == 0) def test_death(): state = GameState(hp=5) parser.apply_state_changes(state, ["HP: -50"]) check("hp floored at 0", state.hp == 0) check("game over on death", state.game_over) def test_combat_flow(): state = GameState() parser.apply_state_changes(state, ["ENEMY: Goblin|hp=10|atk=4"]) check("combat started", state.enemy is not None and state.enemy.name == "Goblin") parser.apply_state_changes(state, ["ENEMY_HP: -6"]) check("enemy damaged", state.enemy.hp == 4) parser.apply_state_changes(state, ["ENEMY_HP: -10"]) check("combat ended on death", state.enemy is None) def test_leveling(): state = GameState(level=1, xp=0, max_hp=20) parser.apply_state_changes(state, ["XP: +10"]) check("leveled up", state.level == 2) check("max hp grew", state.max_hp == 25) def test_unparseable_ignored(): state = GameState(hp=20) before = state.to_dict() parser.apply_state_changes(state, ["HP: lots", "WUT: 5", "random gibberish"]) check("garbage ignored", state.to_dict() == before) def test_full_engine_mock(): engine = GameEngine(build_backend("mock")) opening = engine.start() check("opening has narrative", len(opening.narrative) > 0) check("opening has choices", len(opening.choices) == 3) # force a combat then attack engine.state.start_combat(Enemy("Wraith", hp=10, max_hp=10, attack=3)) res = engine.act("I attack the wraith with my dagger") check("attack damaged enemy or ended combat", engine.state.enemy is None or engine.state.enemy.hp < 10) def main(): tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for t in tests: t() print(f"\nAll {len(tests)} test groups passed.") if __name__ == "__main__": main()