| """The Voices β the actor-context layer. |
| |
| This isn't a separate model call; it's the slice of context that tells the shared LLM *who* |
| is on stage and how they speak. We only ever inject the sheets of the **present** characters |
| (never the whole cast) to respect the token budget and to keep NPCs non-omniscient. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from .schemas import Character, GameState |
| from .utils import clip_words |
|
|
|
|
| def present_characters(state: GameState) -> list[Character]: |
| return [state.characters[cid] for cid in state.scene.present if cid in state.characters] |
|
|
|
|
| def actor_roster(state: GameState) -> str: |
| """One-liner list of who can speak this turn.""" |
| rows = [f"{c.id} = {c.name} ({c.one_line})" for c in present_characters(state)] |
| return "; ".join(rows) if rows else "narrator" |
|
|
|
|
| def present_sheets_block(state: GameState) -> str: |
| """Compact, prompt-friendly sheets for the spirits currently on stage.""" |
| blocks: list[str] = [] |
| for c in present_characters(state): |
| facts = "; ".join(c.known_facts) if c.known_facts else "β" |
| rel = c.relationship |
| if rel <= -100: |
| rel_note = " β οΈ DESPISES the wanderer β refuses all contact, storms off if addressed" |
| elif rel <= -60: |
| rel_note = " β οΈ Very hostile β any unsolicited touch or flirt will make them leave" |
| elif rel <= -20: |
| rel_note = " β cold and resentful; physical contact is unwanted (negative delta)" |
| elif rel < 20: |
| rel_note = " β barely acquainted; unsolicited touch or flirt feels uncomfortable (negative delta)" |
| else: |
| rel_note = "" |
| |
| npc_bond_lines: list[str] = [] |
| for other_id, bond_val in c.npc_relations.items(): |
| if other_id in state.characters: |
| other = state.characters[other_id] |
| note = c.npc_relation_notes.get(other_id, "") |
| label = f" ({note})" if note else "" |
| npc_bond_lines.append(f"{other.name}: {bond_val:+d}{label}") |
| npc_bonds_str = "; ".join(npc_bond_lines) if npc_bond_lines else "β" |
|
|
| blocks.append( |
| |
| |
| f"### {c.name} (id: {c.id})\n" |
| f"- one_line: {clip_words(c.one_line, 30)}\n" |
| f"- traits: {', '.join(c.traits) or 'β'}\n" |
| f"- voice: {clip_words(c.voice, 25) or 'β'}\n" |
| f"- goal: {clip_words(c.goals, 50) or 'β'}\n" |
| f"- mood: {c.mood} | feeling toward wanderer: {rel}/100{rel_note}\n" |
| f"- bonds with others: {npc_bonds_str}\n" |
| f"- knows: {facts}" |
| ) |
| return "\n\n".join(blocks) if blocks else "(no spirits on stage β the narrator may speak)" |
|
|