brad-did-something / tests /test_events.py
qFelix's picture
Brad Did Something - Gradio/FastAPI Space on HF
78c0f6e
Raw
History Blame Contribute Delete
8.34 kB
from game import events, fallbacks, llm, presentation
from game.state import GameState
def make_state(**kw):
s = GameState(session_id="t", phase="free_roam")
for k, v in kw.items():
setattr(s, k, v)
return s
def play_one(state, response_type="quick_explain", text=""):
event = events.next_event(state)
if event["kind"] == "presentation":
# walk every presentation round to completion
data = presentation.advance(state, "", "")
while data["kind"] == "round":
data = presentation.advance(state, "custom",
"We owned it and here is the plan.")
return event, data
outcome = events.respond(state, response_type, text)
return event, outcome
def test_full_quarter_runs():
state = make_state()
seen_presentations = 0
while state.crisis_number < 15 or state.current_event:
event, _ = play_one(state)
if event["kind"] == "presentation":
seen_presentations += 1
assert seen_presentations == 3
assert state.phase == "review"
review = presentation.quarterly_review(state)
assert review["tier"] in ("hit_target", "above_600k", "300k_to_600k",
"below_300k")
assert len(state.event_log) >= 15
def test_presentations_fire_at_4_8_15():
state = make_state()
fired = []
while state.crisis_number < 15 or state.current_event:
event, _ = play_one(state)
if event["kind"] == "presentation":
fired.append(event["crisis_number"])
assert fired == [4, 8, 15]
def test_special_drought_forces_higher_chance():
# over many slots, specials must occur (25% → 60% after 3 dry slots)
state = make_state()
specials = 0
for _ in range(12):
if state.crisis_number >= 15:
break
event, _ = play_one(state)
if event.get("special"):
specials += 1
assert specials >= 1
def test_fine_whatever_tracked():
state = make_state()
event = events.next_event(state)
if event["kind"] == "presentation":
return # slot landed on a presentation; counter logic tested below
events.respond(state, "quick_fine", "")
assert state.consecutive_fine_whatever == 1 or state.current_event is None
def test_bribery_accept_increases_pocket():
state = make_state()
state.queued_special = "bribery"
event = events.next_event(state)
assert event["special"] == "bribery"
offer = event["bribe_offer"]
before = state.pocket_money
events.respond(state, "option_a", "")
assert state.pocket_money == before + offer
assert state.bribes_accepted == 1
def test_bribery_decline_boosts_morale():
state = make_state()
state.queued_special = "bribery"
events.next_event(state)
before = state.morale
events.respond(state, "option_b", "")
assert state.morale == before + 5 and state.bribes_accepted == 0
def test_event_log_grows_and_snapshot_hides_morale():
state = make_state()
play_one(state)
snap = state.snapshot()
assert "morale" not in snap
assert all("relationship" not in str(k) for k in snap)
assert snap["crisis_number"] == state.crisis_number
def _romance_event(state, npc_id):
"""Stage a romance special event as the active crisis."""
state.npc(npc_id).relationship = 72
ev = events._special_event(state, "romance")
ev.update({"kind": "crisis", "special": "romance",
"affected_npc": npc_id, "crisis_number": 2})
state.current_event = ev
state.crisis_number = 2
state.phase = "crisis"
return ev
def test_romance_only_offered_when_eligible():
state = make_state()
assert events._romance_candidate(state) is None # everyone at 50
state.npc("stacey").relationship = 75
assert events._romance_candidate(state) == "stacey"
state.npc("stacey").romance_active = True
assert events._romance_candidate(state) is None # already taken
assert events._any_romance_active(state)
def test_romance_pursue_sets_active():
state = make_state()
_romance_event(state, "stacey")
events.respond(state, "option_a", "")
assert state.npc("stacey").romance_active
snap = state.snapshot()
assert snap["npc_romance"]["stacey"] == "active"
def test_romance_decline_stays_professional():
state = make_state()
_romance_event(state, "kevin")
events.respond(state, "option_b", "")
# the core behavior: declining does NOT start a romance. (The exact
# relationship change is muddied by the crisis outcome's own delta, so we
# don't assert its direction here — the decline penalty is applied in code.)
assert not state.npc("kevin").romance_active
def test_romance_custom_affectionate_pursues():
state = make_state()
_romance_event(state, "janet")
events.respond(state, "custom", "Honestly, I think I'm in love with you too.")
assert state.npc("janet").romance_active
def test_romance_above_80_triggers_hr():
state = make_state()
state.npc("brad").romance_active = True
state.npc("brad").relationship = 78
ev = {"kind": "crisis", "special": None, "affected_npc": "brad",
"crisis_number": 2}
state.current_event = ev
state.crisis_number = 2
state.phase = "crisis"
# a supportive interaction nudges Brad past 80 while dating → HR alert
events.respond(state, "custom",
"You crushed that pitch, genuinely the best on the team.")
if state.npc("brad").relationship > 80:
assert state.hr_alert and state.queued_special == "hr"
def test_snapshot_romance_states():
state = make_state()
state.npc("derek").relationship = 66
snap = state.snapshot()
assert snap["npc_romance"]["derek"] == "available"
assert snap["npc_romance"]["brad"] == "none"
assert "relationship" not in str(snap["npc_romance"]) # no raw number
def test_presentation_wrong_slide_on_active_romance():
state = make_state(crisis_number=3)
state.npc("stacey").romance_active = True
events.next_event(state)
assert state.presentation["presenting_npc"] == "stacey"
assert state.presentation["npc_state"] == "romance"
assert state.presentation["wrong_slide_pending"]
def test_extended_presentation_on_low_morale():
state = make_state(morale=10, crisis_number=3)
event = events.next_event(state)
assert event["kind"] == "presentation"
assert state.presentation["total_rounds"] == 4
def test_call_validated_retries_then_succeeds():
# a single validation slip should trigger ONE retry, not a fallback
state = make_state()
seen = {"n": 0}
def validate(p):
seen["n"] += 1
return None if seen["n"] == 1 else p # fail first, accept second
out = llm.call_validated(state, "event", "sys", "usr", validate,
requested_type="normal", npc_id="brad")
assert out is not None
assert seen["n"] == 2 # it retried exactly once
def test_call_validated_gives_up_after_retry():
state = make_state()
out = llm.call_validated(state, "event", "sys", "usr", lambda p: None,
requested_type="normal", npc_id="brad")
assert out is None # both attempts failed → caller falls back
def test_special_fallback_is_type_matched():
np = fallbacks.special_fallback("newspaper", 0)
assert np["affected_npc"] == "brad" and "press" in np["headline"].lower()
hr = fallbacks.special_fallback("hr", 0)
assert hr["affected_npc"] == "player"
client = fallbacks.special_fallback("client_emergency", 0)
assert "client" in client["headline"].lower()
# unknown kind degrades to the generic bank
assert fallbacks.special_fallback("mystery", 0)["headline"]
def test_presentation_start_is_idempotent():
# repeated empty "start" calls must re-serve round 1, not burn rounds
state = make_state(crisis_number=3)
events.next_event(state)
first = presentation.advance(state, "", "")
again = presentation.advance(state, "", "")
third = presentation.advance(state, "", "")
assert first["round"] == again["round"] == third["round"] == 1
assert state.presentation["round"] == 1
# a real answer still advances
nxt = presentation.advance(state, "custom", "We owned it.")
assert nxt["round"] == 2