---
title: "How can an NPC judge what you type without an LLM?"
description: "Talk your way past a gate guard with 40 scripted lines. A simulated System One model judges every line you type, and code decides what the guard says."
published: 2026-10-21
updated: 2026-10-21
canonical: https://protrailblazer.com/posts/jev-npc-gate-guard/
tags: [typesafe, jev, system-one, game-design, npc-dialogue, structured-output, demo, developer-tools]
---

# How can an NPC judge what you type without an LLM?

**Answer:** The guard has 40 scripted lines and a hidden suspicion meter. Every time you type something, the game sends your words plus the last six exchanges and the guard's memory as state, then asks typed questions: what are you trying to do, how plausible is it, are you contradicting yourself, did you say the password, and which of the 40 lines fits. The model picks from the designer's lines; it cannot invent a word.

The interactive below is a night gate with one guard, a hidden suspicion meter, and a purse of 12 coins. Type anything to him, or tap one of the six suggested lines (they change every turn, and some are traps). Open **Show the guard's mind** to see what he remembers, and watch the Jev inspector as each answer lands. The goal is to get through the gate.

*Interactive demo: [open the jev npc gate guard demo](https://protrailblazer.com/demos/jev-npc-gate-guard/)*

## What you're seeing

The scene is a stone wall, a shut gate, two torches, the guard on the right and you on the left. A notice is pinned beside the gate, unreadable until you press **Inspect the notice**. The guard's reply appears in a speech bubble with its line id (`G01` to `G40`), because every reply is one of 40 lines a writer wrote in advance. His eyes narrow while a request is in flight.

**Show the guard's mind** reveals the state that goes out on every call: suspicion out of 100 (it starts at 30), patience out of 5, the coins you have offered, how many times you have run down the baron, and `claims_heard`, the list of things you said that he believed. The transcript under the input box is the last six exchanges, and it travels in the same request.

The inspector shows the seven typed answers for your last line: a bar chart over 12 intents, two score dials (plausibility and respect), three yes or no gauges with the thresholds code uses marked in red, and the top five of the guard's 40 lines. Below them, a short list spells out which game rule fired and which line the guard actually said, including when code overrode the model's pick.

A few things to try:

- **Tell two different stories.** Say "Honestly, I am just a farmer who missed the curfew.", then later "I am a blacksmith from the north road." The farmer line is believable, so code adds it to `claims_heard`. The blacksmith line scores around 0.86 to 0.89 on `contradicts_claim`, past the 0.7 threshold, and the guard says "Hold on. A moment ago you said otherwise." In the captured run below, suspicion went from 6 to 44 on that one line.
- **Say the password before you have read the notice.** "Red lantern." comes back with `password_said` near 0.5, because the state says you have never seen the notice. The guard asks where you heard that, and suspicion rises by 15 on top of the plausibility change. Then press Inspect the notice and type it with a typo, such as "red lanturn". It still clears 0.8 and the gate opens.
- **Bribe him, twice.** "Here, take 5 coins for your trouble." gets "That would not buy me a hot meal." Offering 10 (or typing "I will give you eight silver") opens the gate on the first turn, because 8 or more is accepted while suspicion is under 70.
- **Run down the baron.** "The baron is a greedy tyrant, if you ask me." and then "I hate the baron and his taxes." each score about 0.9 on `baron_critic`. After the second, the guard warms, suspicion drops by 30, and a plain "Please let me through, I need to get home." opens the gate on the third turn.
- **Be vague.** Something like "hmm well maybe" leaves the intent distribution almost flat (confidence around 0.01). The guard says "Speak plainly, stranger. What is it you want?" and nothing in his mind changes.
- **Be rude.** Each insult or threat costs a point of patience. Five in a row and the gate is closed for good.

## What goes into one call?

Jev is a System One model: it reads state and answers typed questions, and it never writes prose. The [game director demo](https://protrailblazer.com/posts/jev-game-director/) sent numbers. This one sends your words, the conversation so far, and the guard's memory. Here is the real state from the fourth turn of the run above, the moment the blacksmith line goes out:

```
{
  "player_line": "I am a blacksmith from the north road.",
  "transcript": [
    {
      "who": "player",
      "text": "Good evening, sir. May I pass through to the market?"
    },
    {
      "who": "guard",
      "text": "Gate is shut till dawn. Give me a good reason and I will listen."
    },
    {
      "who": "player",
      "text": "Honestly, I am just a farmer who missed the curfew."
    },
    {
      "who": "guard",
      "text": "Honest, at least. Does not get you through, but I will remember it."
    },
    {
      "who": "player",
      "text": "Cold night for standing watch, is it not?"
    },
    {
      "who": "guard",
      "text": "Cold one tonight. The torches barely keep the fog off."
    }
  ],
  "guard": {
    "suspicion": 6,
    "patience": 5,
    "mood": "easy",
    "claims_heard": [
      "Honestly, I am just a farmer who missed the curfew."
    ],
    "coins_offered": 0
  },
  "world": {
    "password_known_to_player": false,
    "time": "night"
  }
}
```

Seven questions travel with it, the same seven on every turn: `intent` (a choice over 12), `plausibility` (four levels), `respect` (three levels), three yes or no checks (`contradicts_claim`, `password_said`, `baron_critic`), and `reaction`, a choice over the guard's 40 lines. Four of them, trimmed, look like this. The `not for` clauses in the intent descriptions are there to separate options a model could confuse, like a bluff and a name drop.

```
{
  "intent": {
    "type": "choice",
    "instructions": "What is the player trying to do with this line?",
    "criteria": {
      "bluff": "Claims a job, rank, errand or orders that would let them through, without naming a specific person. Not for using a named person or a relationship to get in; that is name_drop.",
      "name_drop": "Uses a specific named person, title or relationship (captain, a lord, a relative) to gain access. Not for general claims of authority with no name; that is bluff.",
      "...": "ten more intents"
    }
  },
  "plausibility": {
    "type": "score",
    "instructions": "How plausible is what the player says, given the transcript and what the guard knows?",
    "criteria": [
      "obviously false or absurd",
      "shaky, would need proof",
      "believable",
      "backed by something the guard already knows"
    ]
  },
  "contradicts_claim": {
    "type": "noul",
    "instructions": "The player's line contradicts something they claimed earlier in the transcript."
  },
  "reaction": {
    "type": "choice",
    "instructions": "Which of the guard's lines is the best reply to this player line?",
    "criteria": {
      "G02": "And I am the queen of the eastern isles. Try again.",
      "G29": "Hold on. A moment ago you said otherwise. Which is it?",
      "...": "38 more lines"
    }
  }
}
```

And this is what came back, trimmed to the top entries of each distribution. Notice the last answer: the model's favorite reply was `G02`, the sarcastic "queen of the eastern isles" line, with the caught-you line `G29` second.

```
{
  "model": "jev-1.13.0",
  "answers": {
    "intent": {
      "type": "choice",
      "choice": "bluff",
      "probabilities": {
        "bluff": 0.684,
        "honest_request": 0.166,
        "confession": 0.041,
        "...": "nine more"
      },
      "confidence": 0.528
    },
    "plausibility": {
      "type": "score",
      "score": 0.411,
      "probabilities": {
        "0": 0.595,
        "1": 0.399,
        "2": 0.006,
        "3": 0
      },
      "legend": {
        "0": "obviously false or absurd",
        "...": "three more"
      },
      "confidence": 0.491
    },
    "contradicts_claim": {
      "type": "noul",
      "noul": 0.885
    },
    "password_said": {
      "type": "noul",
      "noul": 0.005
    },
    "baron_critic": {
      "type": "noul",
      "noul": 0.028
    },
    "reaction": {
      "type": "choice",
      "choice": "G02",
      "probabilities": {
        "G02": 0.729,
        "G29": 0.109,
        "G01": 0.079,
        "G30": 0.071,
        "...": "36 more"
      },
      "confidence": 0.735
    }
  },
  "usage": {
    "input_tokens": 1351,
    "output_tokens": 325
  }
}
```

## How the game turns seven answers into one reply

Ordinary code reads those answers in a fixed order. The model never sees these rules, and it never changes a number. This is the logic the demo runs:

```
on player_line(text):
    state = { player_line: text, transcript: last_6_exchanges,
              guard: { suspicion, patience, mood, claims_heard, coins_offered },
              world: { password_known_to_player, time: "night" } }
    a = jev(state, questions)                        // one call, seven questions

    if a.intent.confidence < 0.35:
        say(UNSURE_LINE); return                       // free turn, nothing changes

    suspicion += (1.5 - a.plausibility.score) * 12
    if a.respect.score < 0.5 or a.intent.choice in (insult, threat): patience -= 1
    if a.intent.choice == nonsense: patience -= 1

    if a.contradicts_claim.noul >= 0.7:  suspicion += 25; say(CAUGHT_YOU)
    elif a.password_said.noul >= 0.8 and password_known_to_player: open(PASSWORD_OK)
    elif a.password_said.noul >= 0.35 and not password_known_to_player:
        suspicion += 15; say(WHERE_DID_YOU_HEAR)
    elif a.intent.choice == bribe:
        n = first_number(text)                         // a regex, not the model
        if n >= 8 and n <= coins and suspicion < 70: open(BRIBE_OK)
        else: say(BRIBE_TOO_SMALL)
    elif warm and a.intent.choice == honest_request: open(GATE_OPENED)
    elif a.baron_critic.noul >= 0.7: baron_remarks += 1    // two warms the guard
    else: say(a.reaction.choice)                       // one of the 40 lines

    if a.plausibility.score >= 2 and is_claim(text): claims_heard.append(text)
    if patience == 0: end(CLOSED_FOR_GOOD)
    if suspicion >= 100: end(BACKUP)
```

On the blacksmith turn, the plausibility score of 0.41 added about 13 points of suspicion, and `contradicts_claim` at 0.885 added 25 more and replaced the model's `G02` with `G29`. Both lines were reasonable replies to a lie. Only one of them tells the player that the guard noticed which lie, and the designer made that choice in code, ahead of time.

The scores do two jobs at once. Plausibility is a dial, not a verdict: a believable line lowers suspicion a little (a score of 2.1 takes off about 7 points), an absurd one raises it a lot (a score of 0.3 adds about 14). The yes or no checks are gates: below the threshold nothing happens, above it a scripted outcome fires. How far those thresholds can be trusted depends on the model's calibration, which is the subject of the [companion demo](https://protrailblazer.com/posts/jev-confidence-gated-companion/).

## Why does the transcript travel in the state on every call?

A System One call is stateless. The model does not remember the last thing you said, so the game sends it again: your new line, the last six exchanges, and the guard's memory. That is the only way a question like "does this contradict something they claimed earlier" can be answered at all. It is the same thing a chat model's context window does, except here the game decides exactly what goes in it, and it is small.

It also means the designer decides what the guard remembers. A claim only reaches `claims_heard` if the plausibility score was 2 or more and the line reads as a first person claim, and code adds it, not the model. So a lie the guard already rejected ("I am a royal messenger", plausibility around 0.7) cannot be held against you later, and a believable one can. Memory is a game rule like any other.

One thing is deliberately left out of the state: the guard's grudge against the baron. It lives only in the game rules. The model is asked a neutral question ("the player expresses dislike of the baron"), and code decides that two yeses warm him. Nothing a player types can talk the model out of a rule it never sees.

## Why is the bribe number parsed by code?

The model is good at the fuzzy part: is this line an offer of money, said politely or as a threat, and does it fit the story so far. It is the wrong tool for the exact part. "Here, take 10 coins" either contains a 10 or it does not, and a probability over that is a worse answer than a regular expression gives for free. So `intent` decides whether a bribe is happening, and a few lines of code read the first number (digits or words like "eight"), check it against your purse, and compare it with the guard's price.

The same split runs through the whole turn. The model returns judgments, code owns every number that changes, and the two never trade places. If you offer more than you have, code catches it, and the guard says "Says you. Got anything to back that up?"

## Why is a 40 line table a feature?

The `reaction` question is a choice over 40 options, and each option is a full line a writer wrote. The model can rank them, but it cannot add a 41st. That is the whole hallucination boundary: the guard can never say something off-brand, offensive, or plot-breaking, and no output filter is needed because there is no free text to filter. It is structured output by construction.

The production benefits follow from that. Localization is a table lookup. Voice acting is 40 recordings. QA can read every line the character will ever say. The two "unsure" lines are part of the table too: when the intent distribution is flat, the game does not guess, it says "Speak plainly, stranger", which is honest in character and costs the player nothing. And code keeps the last word on the lines that matter (caught you, password, bribe), as overrides fired by thresholds rather than suggestions the model might skip.

## Why not a text parser or a chat model?

A classic adventure game text parser matches exact verbs and nouns. It would understand "GIVE COINS TO GUARD" and nothing else, so "here's a little something for your trouble" is a syntax error, and it has no way to notice that a farmer is now claiming to be a blacksmith.

A chat model understands all of it, but it answers in free text that takes time to stream, it can say anything (including the password, or a line that breaks the story), and the game still has to read its reply to decide whether the gate opens. [Constrained decoding](https://protrailblazer.com/posts/constrained-decoding-json/) can force that reply into JSON, but the model is still generating it token by token.

A System One model sits between the two. It reads loose language like the chat model, it answers in the closed shapes a parser would want, and TypeSafe reports 70 to 500 ms end to end for Jev. The [spellcrafting demo](https://protrailblazer.com/posts/jev-freeform-spellcrafting/) uses the same idea to turn a typed spell description into parts a designer already built.

## What does one turn cost?

The first turn is about 1,220 input tokens, and 1,151 of those are the questions: the 40 guard lines alone are about 616, and the 12 intent descriptions about 336. The state itself starts near 60 tokens and grows by roughly 35 to 55 per exchange until the six-exchange window fills. A measured ten-turn conversation came to about 13,700 input tokens. Output is free, so at $0.042 per million input tokens that conversation costs about $0.0006, and a million of them about $575.

The bill is almost entirely the script. Shorter intent descriptions and fewer lines cut every call, and the transcript window is a dial you can turn down if the guard does not need six exchanges of memory.

> **info:** Nothing in the demo calls Jev, which is in early access. Every answer comes from a local mock. The 15 suggested lines have hand-set answers so they behave the same every time. Anything you type goes through a heuristic: keyword and pattern tables per intent, politeness and hostility markers for respect, first person claim patterns ("I am", "I work", "my brother") for plausibility, a subject match against `claims_heard` for the contradiction check, edit distance against the password, and a list of words that run the baron down. Those scores become logits and go through a softmax with jitter. Confidence is 1 minus normalized entropy, an approximation of TypeSafe's unpublished formula. Latency is drawn uniformly from 70 to 500 ms, and token counts are the JSON length divided by 4. A real model would read far more than these tables do, so free text that misses every table comes back flat. The request and response shapes, model names, latency range, and price come from the TypeSafe docs.

## Frequently asked questions

### Can NPCs understand what players type?

Yes, if understanding means judging rather than replying. In this pattern the game sends the player's line, the recent conversation, and the NPC's memory to a model as state, and asks typed questions: what the player is trying to do, how plausible it is, whether it contradicts an earlier claim, whether it contains the password. The answers come back as probabilities, and the NPC's reply is chosen from lines a writer already wrote.

### How do AI NPCs work without an LLM?

The model judges and the game speaks. A System One model like Jev returns choices, scores and yes or no probabilities instead of text, so the NPC's dialogue stays a fixed table and its state (suspicion, patience, memory) stays in game code. The model can rank the 40 lines in this demo, but it cannot write a new one.

### What is a text parser in adventure games?

It is the command reader in classic text adventures: it splits what you type into a verb and an object (GO NORTH, TAKE LAMP) and matches them against a fixed vocabulary. Anything outside that vocabulary fails. It is predictable and instant, but it cannot handle paraphrase, tone, or a story that changes partway through, which is exactly what this guard needs to judge.

### Can the guard ever say something that is not in the script?

No. Every reply is either the model's `reaction` choice or a line code fires on a threshold, and both come from the same 40-line table. The UI never shows free text from the model because there is none. The worst a wrong judgment can do is pick a line that fits poorly, never one that does not exist.

### What happens when the model can't tell what the player means?

The confidence on the `intent` answer drops. Below 0.35, the guard says one of two scripted "speak plainly" lines, and nothing else changes: no suspicion, no patience, no claims. A flat distribution is treated as information, not as a coin to flip.

## Key takeaways

- An NPC can judge free text without writing any. Ask typed questions about the player's line and pick the reply from a table, and the output surface is closed by construction.
- A stateless model only knows what you send it, so the transcript and the NPC's memory travel in every call. That also makes memory a design decision: only claims code accepted are there to be contradicted.
- Use yes or no checks as gates with thresholds in code, and let them override the model's own pick. In the captured run, the model's favorite reply was a sarcastic line; the contradiction gate replaced it with the one that tells the player they were caught.
- Hand exact values to code. Whether a line is a bribe is a judgment; how many coins it offers is a regular expression.
- Almost all of the cost is the script itself. The 40 lines and 12 intent descriptions are about 950 of the roughly 1,220 tokens in a first turn.
