you-are-a-bug / test_engine.py
jnalv's picture
Upload folder using huggingface_hub
1082b22 verified
Raw
History Blame Contribute Delete
5.34 kB
"""Unit tests for engine.py — the code-owned rules layer. No model needed.
Run from app/: python -m pytest test_engine.py
"""
import json
import engine
# --- parse_output: the lenient §13 parser -----------------------------------
def test_parse_valid_say():
raw = json.dumps({"action": "say", "text": "The moss is cool."})
assert engine.parse_output(raw) == {"action": "say", "text": "The moss is cool."}
def test_parse_valid_roll():
obj = {
"action": "roll", "stat": "might", "difficulty": "hard",
"on_success": "It moves.", "on_fail": "It does not.",
}
assert engine.parse_output(json.dumps(obj)) == obj
def test_parse_json_with_surrounding_prose():
raw = 'Sure! {"action": "say", "text": "A beetle hums."} hope that helps'
assert engine.parse_output(raw)["text"] == "A beetle hums."
def test_parse_garbage_degrades_to_say():
out = engine.parse_output("the forest just vibes, no json here")
assert out["action"] == "say"
assert "vibes" in out["text"]
def test_parse_bad_stat_degrades_to_say():
obj = {
"action": "roll", "stat": "charisma", "difficulty": "hard",
"on_success": "yes", "on_fail": "no",
}
assert engine.parse_output(json.dumps(obj))["action"] == "say"
def test_parse_empty_degrades_to_say():
out = engine.parse_output("")
assert out["action"] == "say"
assert out["text"]
def test_parse_truncated_roll_recovers_roll():
# Cut off mid-on_fail (max_tokens truncation): both narrations recoverable.
raw = (
'{"action": "roll", "stat": "speed", "difficulty": "medium", '
'"on_success": "You skitter clear.", "on_fail": "The leaf snaps and you tum'
)
out = engine.parse_output(raw)
assert out["action"] == "roll"
assert out["stat"] == "speed"
assert out["difficulty"] == "medium"
assert out["on_success"] == "You skitter clear."
assert "leaf snaps" in out["on_fail"]
def test_parse_truncated_roll_without_on_fail_degrades_to_say():
raw = (
'{"action": "roll", "stat": "might", "difficulty": "hard", '
'"on_success": "The log shifts'
)
out = engine.parse_output(raw)
assert out["action"] == "say"
assert "log shifts" in out["text"]
def test_parse_roll_with_unescaped_quotes_salvages():
raw = (
'{"action": "roll", "stat": "smarts", "difficulty": "easy", '
'"on_success": "The ant says "fine" and moves.", "on_fail": "The ant balks."}'
)
out = engine.parse_output(raw)
assert out["action"] == "roll"
assert "fine" in out["on_success"]
assert out["on_fail"] == "The ant balks."
def test_parse_log_tagged_lines_keep_narration():
raw = "[dm · Might check, Easy, failed] The bark holds firm against you."
out = engine.parse_output(raw)
assert out["action"] == "say"
assert "bark holds firm" in out["text"]
def test_parse_headers_then_prose_keeps_narration():
raw = "PLAYER'S TURN\nsay\nA snail watches you with mild interest."
out = engine.parse_output(raw)
assert out["action"] == "say"
assert out["text"] == "A snail watches you with mild interest."
# --- dice ---------------------------------------------------------------------
def test_resolve_roll_bounds():
# 2d6 is 2..12; stat 10 always beats easy DC 6, stat -10 never reaches hard DC 10.
assert all(engine.resolve_roll(10, "easy") == "success" for _ in range(50))
assert all(engine.resolve_roll(-10, "hard") == "fail" for _ in range(50))
def test_resolve_roll_returns_both_outcomes():
results = {engine.resolve_roll(2, "medium") for _ in range(300)}
assert results == {"success", "fail"}
# --- damage / parity with training ---------------------------------------------
def test_band_damage_matches_exploder():
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import explode_transcript
assert engine.BAND_DAMAGE == explode_transcript.BAND_DAMAGE
def test_apply_damage_floors_at_zero():
sheet = {"hp": {"cur": 2, "max": 8}}
assert engine.apply_damage(sheet, "hard")["hp"]["cur"] == 0
assert engine.apply_damage(sheet, "easy")["hp"]["cur"] == 2
# --- XP ladder / level-up ---------------------------------------------------------
def test_level_for_xp_ladder():
assert engine.level_for_xp(0) == 1
assert engine.level_for_xp(3) == 1
assert engine.level_for_xp(4) == 2
assert engine.level_for_xp(10) == 3
assert engine.level_for_xp(999) == 7
def test_maybe_level_up_carries_damage():
import roster
bug_id = sorted(roster.load().keys())[0]
sheet = roster.load_at_level(bug_id, 1)
sheet["hp"]["cur"] = max(1, sheet["hp"]["cur"] - 2) # wounded
old_cur = sheet["hp"]["cur"]
old_max = sheet["hp"]["max"]
leveled, notes = engine.maybe_level_up(sheet, engine.XP_LADDER[2])
assert leveled["level"] == 2
assert leveled["hp"]["max"] == old_max + 1
assert leveled["hp"]["cur"] == old_cur + 1 # gained the +1, kept the wound
assert notes and "level 2" in notes[0]
def test_maybe_level_up_noop_below_threshold():
import roster
bug_id = sorted(roster.load().keys())[0]
sheet = roster.load_at_level(bug_id, 1)
same, notes = engine.maybe_level_up(sheet, engine.XP_LADDER[2] - 1)
assert same is sheet
assert notes == []