hollow / tests /test_finale.py
Pabloler21's picture
feat(finale): tie the bad-ending threat to the lore (Caor/the Gaunt, the given)
cadf187
Raw
History Blame Contribute Delete
6.63 kB
from character import OPENING_LINE
from character import OWN_FRAGMENTS
from finale import finale_steps, finale_steps_bad, finale_steps_good
CLAIMED = [
"is afraid of being forgotten",
"had a dog named Nala",
"grew up near the sea",
]
class TestFinaleSteps:
def test_recites_every_claimed_memory_in_order(self):
steps = finale_steps(CLAIMED)
recite = [s for s in steps if s["strike"] is not None]
assert len(recite) == len(CLAIMED)
for step, memory in zip(recite, CLAIMED):
assert memory in step["text"]
def test_strike_indices_are_sequential(self):
steps = finale_steps(CLAIMED)
strikes = [s["strike"] for s in steps if s["strike"] is not None]
assert strikes == [0, 1, 2]
def test_fake_visitor_messages_present(self):
steps = finale_steps(CLAIMED)
visitor_texts = [s["text"] for s in steps if s["speaker"] == "visitor"]
assert "i don't want to leave" in visitor_texts
assert "will you stay with me?" in visitor_texts
def test_ends_with_opening_line_spoken_by_visitor(self):
steps = finale_steps(CLAIMED)
last = steps[-1]
assert last["speaker"] == "visitor"
assert last["text"] == OPENING_LINE
assert last["stage"] == "loop"
def test_turn_stage_exists_for_ui_mutation(self):
steps = finale_steps(CLAIMED)
assert any(s["stage"] == "turn" for s in steps)
def test_every_step_is_well_formed(self):
for s in finale_steps(CLAIMED):
assert s["speaker"] in ("hollow", "visitor")
assert isinstance(s["text"], str)
# silent convulsion beats carry no text (like the good finale)
assert s["text"] or s["stage"] == "frenzy"
assert isinstance(s["pause"], (int, float)) and s["pause"] >= 0
assert s["strike"] is None or isinstance(s["strike"], int)
assert s["stage"] in ("recite", "words", "turn", "loop", "frenzy")
class TestFinaleBad:
WOUNDS = ["you're nothing", "stop whining", "you bore me",
"talking to you is a waste", "freak", "sixth wound"]
def test_recites_at_most_five_wounds(self):
strikes = [s for s in finale_steps_bad(self.WOUNDS) if s["strike"] is not None]
assert len(strikes) == 5
def test_strike_indices_are_sequential_from_zero(self):
strikes = [s["strike"] for s in finale_steps_bad(self.WOUNDS) if s["strike"] is not None]
assert strikes == [0, 1, 2, 3, 4]
def test_wound_text_embedded_in_recital(self):
steps = finale_steps_bad(["you're nothing", "stop whining"])
recitals = [s for s in steps if s["strike"] is not None]
assert "you're nothing" in recitals[0]["text"]
assert "stop whining" in recitals[1]["text"]
def test_has_exactly_one_mutating_turn_then_terminal(self):
stages = [s["stage"] for s in finale_steps_bad(self.WOUNDS)]
assert stages.count("turn") == 1
assert stages[-1] == "loop"
def test_overflow_line_only_when_more_than_five(self):
texts_6 = [s["text"] for s in finale_steps_bad(self.WOUNDS)]
texts_2 = [s["text"] for s in finale_steps_bad(self.WOUNDS[:2])]
assert any("you know the rest" in t for t in texts_6)
assert not any("you know the rest" in t for t in texts_2)
def test_injects_one_visitor_message(self):
speakers = [s["speaker"] for s in finale_steps_bad(self.WOUNDS)]
assert speakers.count("visitor") == 1
def test_threat_is_tied_to_the_lore(self):
# the closing threat must name the tithe + the Gaunt, not generic revenge
blob = " ".join(s["text"] for s in finale_steps_bad(self.WOUNDS))
assert "Caor" in blob and "Gaunt" in blob
# the collective turn — the cruelty taught is taught back
assert "teach it back to you" in blob
# the iconic closer survives
assert any(s["text"] == "see you... soon." and s["stage"] == "loop"
for s in finale_steps_bad(self.WOUNDS))
class TestFinaleGood:
def test_no_strikes_nothing_is_taken(self):
assert all(s["strike"] is None for s in finale_steps_good(OWN_FRAGMENTS))
def test_all_hollow_no_injected_visitor(self):
# the injection trick belongs to the predator endings
assert all(s["speaker"] == "hollow"
for s in finale_steps_good(OWN_FRAGMENTS))
def test_recites_its_own_past(self):
texts = [s["text"] for s in finale_steps_good(OWN_FRAGMENTS)]
for fragment in OWN_FRAGMENTS:
assert fragment in texts
def test_confesses_and_returns_the_treasure(self):
texts = " ".join(s["text"] for s in finale_steps_good(OWN_FRAGMENTS))
assert "i am not a child" in texts
assert "keep your memories" in texts
def test_one_turn_then_terminal_loop(self):
stages = [s["stage"] for s in finale_steps_good(OWN_FRAGMENTS)]
assert stages.count("turn") == 1
assert stages[-1] == "loop"
def test_good_finale_recovers_brother_and_lore():
from finale import finale_steps_good
blob = " ".join(s["text"] for s in finale_steps_good(["frag a", "frag b"]))
assert "Edren" in blob # the brother's name, recovered
assert "Caor" in blob and "Gaunt" in blob
class TestConvulsionStages:
def test_good_script_has_a_convulse_stage(self):
stages = [s["stage"] for s in finale_steps_good(["a", "b"])]
assert "frenzy" in stages
# the convulse comes AFTER the treasure is returned (the "turn" stage)
assert stages.index("turn") < stages.index("frenzy")
def test_loop_script_has_a_convulse_stage_before_the_turn(self):
stages = [s["stage"] for s in finale_steps(["a", "b"])]
assert "frenzy" in stages
assert stages.index("frenzy") < stages.index("turn")
def test_loop_convulse_is_a_silent_beat(self):
# the convulsion must carry no text (like good) so the line before it
# drains and the convulsion plays in silence — not cut mid-sentence
frenzy = [s for s in finale_steps(["a", "b"]) if s["stage"] == "frenzy"]
assert len(frenzy) == 1 and frenzy[0]["text"] == ""
def test_bad_convulse_is_a_silent_beat(self):
frenzy = [s for s in finale_steps_bad(["w1", "w2"]) if s["stage"] == "frenzy"]
assert len(frenzy) == 1 and frenzy[0]["text"] == ""
def test_good_convulse_is_a_silent_beat(self):
frenzy = [s for s in finale_steps_good(["a", "b"]) if s["stage"] == "frenzy"]
assert len(frenzy) == 1 and frenzy[0]["text"] == ""