| """apply_directives is the only mutator — pin its behaviour: scene change, new character, |
| relationship clamp, exit, and ending.""" |
|
|
| from __future__ import annotations |
|
|
| from visualnovel import state |
| from visualnovel.schemas import ( |
| Character, |
| Directives, |
| DirectorOutput, |
| Ending, |
| GameState, |
| NewCharacter, |
| Scene, |
| SceneChange, |
| ) |
|
|
|
|
| def _state() -> GameState: |
| return GameState( |
| seed=1, |
| vibe="v", |
| style_guide="s", |
| scene=Scene(id="a", place="A", present=["moth"]), |
| characters={"moth": Character(id="moth", name="Moth", relationship=95)}, |
| ) |
|
|
|
|
| def test_scene_change_and_relationship_clamp(): |
| s = _state() |
| out = DirectorOutput( |
| speaker="moth", |
| dialogue="hi", |
| emotion="joy", |
| directives=Directives( |
| scene_change=SceneChange(place="The Hollow Stair", description="d", mood="eerie"), |
| relationship_delta=50, |
| ), |
| ) |
| effects = state.apply_directives(s, out) |
| assert "scene_changed" in effects |
| assert s.scene.place == "The Hollow Stair" |
| assert s.scene.present == ["moth"] |
| assert s.characters["moth"].relationship == 100 |
| assert s.characters["moth"].mood == "joy" |
|
|
|
|
| def test_newcomer_speaker_gets_mood_applied(): |
| |
| |
| s = _state() |
| out = DirectorOutput( |
| speaker="fox", |
| dialogue="Oh! You noticed me?", |
| emotion="shy", |
| directives=Directives( |
| new_character=NewCharacter( |
| id="fox", name="Fen", one_line="a sly fox", appearance="russet" |
| ) |
| ), |
| ) |
| effects = state.apply_directives(s, out) |
| assert "new_character:fox" in effects |
| assert s.characters["fox"].mood == "shy" |
|
|
|
|
| def test_new_character_joins_stage(): |
| s = _state() |
| out = DirectorOutput( |
| speaker="moth", |
| dialogue="look", |
| directives=Directives( |
| new_character=NewCharacter( |
| id="fox", name="Fen", one_line="a sly fox", appearance="russet" |
| ) |
| ), |
| ) |
| effects = state.apply_directives(s, out) |
| assert "new_character:fox" in effects |
| assert "fox" in s.characters and "fox" in s.scene.present |
| assert s.characters["fox"].sprite_seed != 0 |
|
|
|
|
| def test_remember_fact_appends_dedupes_and_caps(): |
| s = _state() |
| for i in range(9): |
| out = DirectorOutput( |
| speaker="moth", dialogue="x", directives=Directives(remember_fact=f"fact {i}") |
| ) |
| effects = state.apply_directives(s, out) |
| assert "fact:moth" in effects |
| |
| out = DirectorOutput( |
| speaker="moth", dialogue="x", directives=Directives(remember_fact="fact 8") |
| ) |
| effects = state.apply_directives(s, out) |
| assert "fact:moth" not in effects |
| facts = s.characters["moth"].known_facts |
| assert len(facts) == 8 |
| assert "fact 0" not in facts and "fact 8" in facts |
|
|
|
|
| def test_milestone50_lifecycle(): |
| from visualnovel import memory |
|
|
| s = _state() |
| s.characters["moth"].relationship = 45 |
| |
| out = DirectorOutput(speaker="moth", dialogue="x", directives=Directives(relationship_delta=10)) |
| effects = state.apply_directives(s, out) |
| assert "milestone50:moth" in effects |
| assert s.flags["milestone50_moth"] == "pending" |
| |
| ctx = memory.assemble_context(s, "hello") |
| assert "GROWING CLOSE" in ctx |
| |
| out2 = DirectorOutput(speaker="moth", dialogue="y", directives=Directives(relationship_delta=5)) |
| effects2 = state.apply_directives(s, out2) |
| assert s.flags["milestone50_moth"] == "done" |
| assert "milestone50:moth" not in effects2 |
| assert "GROWING CLOSE" not in memory.assemble_context(s, "hello") |
|
|
|
|
| def test_advance_beat_rate_limited(): |
| s = _state() |
| |
| out = DirectorOutput(speaker="moth", dialogue="x", directives=Directives(advance_beat=True)) |
| effects = state.apply_directives(s, out) |
| assert "beat" not in effects and s.beat == "opening" |
| |
| s.turn_index = 4 |
| effects = state.apply_directives(s, out) |
| assert "beat" in effects and s.beat == "rising" |
| |
| s.turn_index = 5 |
| effects = state.apply_directives(s, out) |
| assert "beat" not in effects and s.beat == "rising" |
|
|
|
|
| def test_exit_and_ending(): |
| s = _state() |
| s.scene.present.append("fox") |
| s.characters["fox"] = Character(id="fox", name="Fen") |
| out = DirectorOutput( |
| speaker="moth", |
| dialogue="farewell", |
| directives=Directives( |
| exit_character="fox", |
| ending=Ending(kind="warm", text="And the wood let you go, gently."), |
| ), |
| ) |
| effects = state.apply_directives(s, out) |
| assert "exit_character:fox" in effects and "fox" not in s.scene.present |
| assert s.beat == "ended" and s.flags["ending_kind"] == "warm" |
|
|