Let the game decide a conversation
Goal
A talking character (Convorcher) whose conversation has an outcome the game owns: a gatekeeper who questions the player, after which the game, not the model's words, decides to let them through or refuse, gives a quick retry on refusal, and never counts a slow or failed reply against the player. Builds on Talk to a character.
Files you will edit
src/guard.story.yamlsrc/systems/talk.ts
Steps
Objectives as signals. In the story file, one objective per thing the game wants to know, each a
player_statecondition the service judges after every turn. They are evidence for the game, not decisions: the character is told to question and react, never to rule.yamlphases: questioning: description: 'The player wants to pass. Question them; notice whether their story holds together.' tone: 'calm, watchful' max_turns: 0 objectives: - id: told-true-reason description: 'The player gave the true reason: medicine for a sick relative.' completion: { type: player_state, condition: 'The player has said they must fetch medicine for a sick relative.', } urgency: normal - id: stayed-consistent description: 'Asked again, the player kept to the same reason.' completion: { type: player_state, condition: 'The player has repeated or expanded the same reason without changing it, after being questioned.', } urgency: normal - id: contradicted description: 'The player changed their story, lied or threatened.' completion: { type: player_state, condition: 'The player has contradicted an earlier answer, admitted a lie, or threatened the character.', } urgency: normalThe personality ends with: "Never say whether they may pass; the decision is not yours."
The verdict, in the game. In
src/systems/talk.ts, a small state machine: count the player's real turns (a reply came back), decide on story snapshots, and give up after a few turns.tsexport const verdict = { state: 'talking' as 'talking' | 'allowed' | 'refused', turns: 0, asked: false, }; const MAX_TURNS = 6; function decide(story: ConversationStory): void { if (verdict.state !== 'talking') return; const done = story.completedObjectives; if (done.includes('contradicted')) verdict.state = 'refused'; else if (done.includes('told-true-reason') && done.includes('stayed-consistent')) verdict.state = 'allowed'; else if (verdict.turns >= MAX_TURNS) verdict.state = 'refused'; }In the event loop of
talkSystem:tsif (kind === 'input' && verdict.state === 'talking') { verdict.asked = true; conversation.command(talkState.npc, 'ask', TALK_ID, text); } else if (kind === 'status' && text === 'ready' && verdict.asked) { verdict.asked = false; verdict.turns += 1; // a reply came back: that was one of the player's turns } else if (kind === 'error' && text.startsWith('retry:')) { verdict.asked = false; // the service's fault: not a turn, and the HUD says "ask again" } else if (kind === 'story') { const story = conversation.story(text); if (story && story.revision > talkState.lastRevision) { talkState.lastRevision = story.revision; decide(story); } }Import
ConversationStoryfromgameablewithconversation.Act on it. When
verdict.stateturnsallowed, end the talk and open the way (an end card, a door, the next level). Onrefused, show it, then a quick retry: end the talk, resetverdictandtalkState.lastRevision, and start again from the same spot.startwith the textfreshbegins the story again from its first phase (without it, the character picks up where the last session was).tsexport function retry(): void { const npc = talkState.npc; conversation.command(npc, 'end', TALK_ID); verdict.state = 'talking'; verdict.turns = 0; verdict.asked = false; talkState.lastRevision = -1; talkState.open = true; conversation.command(npc, 'start', TALK_ID, 'fresh'); }
Verify
- Give a vague answer, then the true one, then repeat it when questioned:
allowedafter the snapshot that completes both objectives (snapshots can come a turn after the reply). - Change your story halfway:
refused, then the retry starts a fresh conversation. - Cut the network mid-reply: an
errorwithretry:and no turn counted.
Notes
- Snapshots lag a turn behind the reply: decide on a new
revision, never onready. - The model's words never decide anything. If the character announces a decision anyway, tighten the personality; the game's state is still what happens.
See also
- Talk to a character — the talk system this builds on
- Write a character's story — objectives and the personality
- Talking characters — why the game, not the model, decides