| 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": |
| |
| 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(): |
| |
| 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 |
| 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 |
| state.npc("stacey").relationship = 75 |
| assert events._romance_candidate(state) == "stacey" |
| state.npc("stacey").romance_active = True |
| assert events._romance_candidate(state) is None |
| 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", "") |
| |
| |
| |
| 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" |
| |
| 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"]) |
|
|
|
|
| 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(): |
| |
| state = make_state() |
| seen = {"n": 0} |
|
|
| def validate(p): |
| seen["n"] += 1 |
| return None if seen["n"] == 1 else p |
|
|
| out = llm.call_validated(state, "event", "sys", "usr", validate, |
| requested_type="normal", npc_id="brad") |
| assert out is not None |
| assert seen["n"] == 2 |
|
|
|
|
| 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 |
|
|
|
|
| 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() |
| |
| assert fallbacks.special_fallback("mystery", 0)["headline"] |
|
|
|
|
| def test_presentation_start_is_idempotent(): |
| |
| 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 |
| |
| nxt = presentation.advance(state, "custom", "We owned it.") |
| assert nxt["round"] == 2 |
|
|