File size: 3,403 Bytes
7fe39f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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 = (
        "<narrative>You enter a dim hall.</narrative>"
        "<state>\nHP: -5\nGOLD: +10\nITEM_ADD: Torch\nLOCATION: Dim Hall\n</state>"
        "<choices>\n1. Go north.\n2. Light the torch.\n</choices>"
    )
    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()