NPC stances: data model
Sketch. Nothing here is built. The working example is misc/labs/yarn-editor/examples/concepts-as-objects/1.yarn; see Quell integration for where this sits in Quell. Everything is hand-authored: no language model at play time or for authoring.
The player collects statements (“A is B”), links two of them into an argument (“A is B because C is D”) in the thinking editor, and presents it to an NPC. Every character holds stances on statements and on families of arguments. Responses follow from those stances by rule, so most of the dialogue is written per situation, not per argument.
The pieces
classDiagram
direction TB
class Registry {
<<Asset / Resource>>
concepts: ConceptId to name, description
properties: PropertyId to name, description
statements: list of Statement
relations: property or concept pair to LeverFamily
contraries: property pairs that can't both hold
}
class Statement {
<<value>>
concept: ConceptId
property: PropertyId
predicate: F or V
subject: s, p or u
}
class Argument {
<<value>>
conclusion: Statement
premise: Statement
form() alpha or beta
family() LeverFamily or none
}
class Stance {
<<enumeration>>
Affirm
Deny
Suspended
}
class Character {
<<entity>>
}
class Stances {
<<component>>
statements: Statement to Stance
families: LeverFamily to Stance
ranking: value order
}
class Inventory {
<<component, player only>>
statements
concepts
properties
}
Registry --> Statement
Argument --> Statement : conclusion, premise
Character --> Stances
Character --> Inventory
Stances ..> Stance : value
Argument ..> Registry : family looked up in| type | Bevy kind | why |
|---|---|---|
Registry | RON asset, inserted as a resource once loaded | The authored world of claims: every valid statement with its tags, and every meaningful relation between predicates or subjects. |
Statement | plain Copy value with interned ids | A concept–property pair with its PTA tags baked in. F/V is the predicate type in context, s/p/u the subject type (“this cup”, “some cups”, “all cups”), so they belong to the statement, not to the concept or property. |
Argument | plain Copy value | Two statements. Form and lever family are derived, never stored. |
Stance | enum, three variants | A missing entry means never examined. Suspended means held, then lost to argument (see Aporia). The two are kept apart so that never committing to anything can’t pass for letting go. |
Stances | component on every character entity | Authored core beliefs plus opinions on argument families. The player has one too; that’s how the game tracks the player’s own mind. |
Inventory | component on the player | Statements the player holds. A statement is acquired whole (found in a text, heard in dialogue) or composed from a held concept and property, but only if the pair is in the registry, so every statement is authored and tagged either way. |
Three places to disagree
An argument can be resisted at the conclusion, the premise, or the link between them (the PTA lever). “I agree A is B, but not because of that” splits in two:
| the character says | A is B | C is D | the link |
|---|---|---|---|
| “A is B, but C is not D” | Affirm | Deny | — |
| “A is B, and C is D, but that’s not why” | Affirm | Affirm | Deny |
Statement stances cover the first two columns. Link stances are held per lever family (cause, example, analogy, authority, …), not per argument: an NPC who rejects “length makes things great” rejects it for every concept.
Form and family
The tags and the registry decide what kind of argument the player made.
Form follows from the shared slot: the same concept is α (“a is X because a is Y”), the same property is β (“a is X because b is X”). A pair sharing neither is γ, and the thinking editor doesn’t accept it (see the counting below for why).
Substance comes from the baked tags: for α the predicate types of conclusion then premise (FF, FV, VF, VV), for β the subject types (su, ss, …).
Lever family: form and substance narrow the options but don’t pick one; “a is great because a is long” is αVF either way. The lever is the relation between the two predicates (α) or the two subjects (β), so it’s authored once per related pair in relations and reused across every argument that pair appears in. A pair with no authored relation has no lever: “that has nothing to do with it.”
How much to write
Symbols
| symbol | meaning |
|---|---|
| authored statements | |
| number of concepts, number of properties | |
| statements whose concept is | |
| statements whose property is | |
| number of NPCs | |
| lever families |
Arguments the player can make
An argument is an ordered pair (conclusion, premise) of distinct statements. α pairs share a concept, β pairs share a property. Two distinct statements can’t share both.
With statements spread evenly (, ):
The growth is quadratic in statements per concept and only linear in the number of concepts. Adding new concepts is cheap; piling claims onto one concept is what explodes. Allowing γ would bring back all pairs, which is the reason to leave it out.
Relations (levers)
One relation per related pair of properties sharing a concept, and per related pair of concepts sharing a property:
This is an upper bound. Most pairs have no meaningful relation and aren’t written. The quadratic term is still here, but it’s paid once for the whole game, not once per NPC.
One NPC
An NPC is a partial map of core beliefs , a partial map of family opinions , and a value ranking. Presenting the argument (conclusion , premise , family ) looks up three values, each Affirm, Deny or missing: situations. Checked in order, they fall into seven response classes:
| premise | link | conclusion | response class |
|---|---|---|---|
| D | any | any | counter the premise: “C is not D” |
| missing | any | any | ask for support: “why is C D?” |
| A | D | any | reject the link: “that’s not a reason” |
| A | missing | any | probe the link: “how does that follow?” |
| A | A | A | agree: “yes, and…” |
| A | A | missing | convinced: adopts A is B |
| A | A | D | conflict: the value ranking decides |
“Ask for support” is how chains happen: the player answers with an argument whose conclusion is the premise C is D.
Suspended counts as missing in this table. Whether a single argument can re-establish a suspended stance is open (see Aporia).
Per NPC, then:
where is the number of variants written per response class (slots fill in the statements) and the bespoke lines for arguments that matter to the story.
Total
Compare writing a response per argument per NPC: , quadratic per concept and multiplied by every NPC.
Worked example: one small debate
20 statements over 5 concepts and 10 properties, evenly spread (, ), one NPC. That’s about one in-world text and one conversation.
| item | count | how |
|---|---|---|
| arguments the player can make | 80 of 380 | |
| definitions | 15 | |
| statements | 20 | |
| relations, at most | 40 | |
| NPC core beliefs | 15 | guess |
| NPC family opinions | 4 | one per family |
| response variants | 14 | |
| bespoke lines | 10 | guess |
| total | 120 short items |
Per-argument writing for the same slice is 80 responses, but every new NPC adds another 80; under the rule, a second NPC adds about 45.
Minds that change
Responses are recomputed from the current stances every time, so changing a stance costs no extra writing for the arguments themselves. What it costs:
- an update rule: convinced sets ; conflict consults the ranking;
- a few lines per NPC acknowledging a change (“…fine. You’ve convinced me.”);
- testing: which stance states are actually reachable.
The machinery is symmetric. The player has Stances too, so an NPC presenting an argument to the player uses the same model with the roles swapped, and the player’s stances become the record of their change of mind. Changing NPCs’ minds and changing the player’s mind can both be tried on the same data.
Ripples and contradictions
A change of mind shouldn’t recompute everything (minds become volatile) or nothing (one belief never touches another). How far it ripples depends on how much the person knows about the relations between their ideas. Two ways to model that:
- Known relations. Each NPC knows a subset of the registry’s relations; a change propagates only along those. Authoring is a list of relation ids per NPC, and the ripple is derived.
- Authored ripples. For each statement an NPC can be convinced of, list the stances that change with it. Full control, more writing: one list per convincible statement, and the response table shows which those are.
Either way, some beliefs are left behind, and the player can point that out. That needs a second kind of relation: contrary properties, which can’t both hold of the same subject (“is a virtue” / “is a weakness”). An NPC is inconsistent when it affirms and with and contrary. That’s mechanical to find, and the count is small: one check per contrary pair per concept.
Pointing out an inconsistency isn’t δ. δ supports a claim by who says it (“Einstein said so”). “You believe X is Y and X is Z, which can’t both be true” is a challenge to the NPC’s commitments: a dialogue move, not a PTA argument type. Dialogue-game theory calls the set of claims a party is committed to its commitment store (Walton and Krabbe, Commitment in Dialogue, 1995). Recast as an argument, it’s α with a negative conclusion (“X is not Y, because X is Z”, contrariety as the lever), which needs the negation question below settled first.
Aporia
A belief can’t be dropped at will. It’s dropped when argumentation breaks its hold: the character can no longer affirm or deny it. That’s Suspended, distinct from a missing entry (never examined). “I’m not sure” early in the game is a missing entry; aporia comes late, and only through argument. It’s a release of dukkha, not passivity.
Two routes lead there, both built on contrary relations:
- Elenchus (the Socratic route). Someone shows the character that its own stances collide: it affirms and with and contrary. Neither is simply flipped; both are suspended.
- Equipollence (the Pyrrhonist route, Sextus Empiricus’s isostheneia). The character has accepted arguments of equal force on both sides: an argument for and one for , and contrary. Neither wins, so judgement is suspended (epoché), and then comes tranquillity (ataraxia).
Both need accumulated commitments to collide, so aporia can’t happen early, and it can’t happen to a character who never committed to anything. Nothing has to script it as late. The held stances are Wittgenstein’s ladder: climbed, then thrown away (see the ladder).
For the player, suspended stances are what the ending checks: see Quell story progression: design.
γ (no shared slot) and δ (arguments from authority, “Einstein said so”) are out: γ for the counting above, δ because a subject would have to be a statement. Both add complexity the game doesn’t need.
P (policy) is out too: statements are F or V. Debates settle what is the case and how good or bad it is, never what to do. Decisions are authored choices offered outside the debate, built from the conversation so far; the player picks one and doesn’t make one up. Letting players argue for policies would mean an open-ended space of behaviour on top of the space of beliefs. “You shouldn’t lie” can still be voiced, typed as the V statement “lying is wrong”.
Presenting an argument
flowchart LR
INV["player Inventory"] -->|pick conclusion, premise| BUILD["build Argument"]
REG[("Registry")] -->|form, family| BUILD
BUILD --> NPC["NPC's Yarn node"]
NPC -->|"response_class(…)"| Q["query system:<br/>reads Stances"]
Q -->|class| NPC
NPC -->|"hold_stance, learn_*"| CMD["command systems:<br/>write Stances, Inventory"]Both directions work in bevy_yarnspinner 0.9 (vendored in bin/vn-jun-26/third_party/YarnSpinner-Rust). Commands are registered systems taking In<T>, and so are functions: examples/bevy_yarnspinner/src/bin/custom_function.rs registers get_counter(counter: Res<Counter>) -> u32 with add_function. So a response_class function can read Stances directly and the Yarn node branches on the class. Quell doesn’t depend on it yet.
Yarn passes strings, so ids are interned at that boundary and nowhere else.
Background
The F/V/P split comes from debate theory, where it classifies propositions by what they ask of the audience. ATIP v6 cites Freeley and Steinberg, Argumentation and Debate (12th ed., 2009), pp. 55–58; Hinton and Wagemans also cite Broda-Bahm et al. (2004) and Skorupski (2010). ATIP applies it to the predicate of an alpha statement, and the s/p/u split (from Aristotle’s De interpretatione) to the subject of a beta statement. Quell keeps only F and V.
Which properties fit which concepts is a separate problem, category mistakes; the closest systematic treatment is Sommers’ predicability trees (Types and Ontology, 1963). The registry’s statement list is the hand-authored version of that tree.
See also
- Quell story progression: design — how the player’s stances gate the story, and why the ending checks for suspended stances.
- Debate — the debate mechanic this model serves.
- Quell integration — the player’s loop, the runtime pieces and
q_pta. - Periodic Table of Arguments — form, substance and lever, in full.
- PTA cheat sheet — PTA 3.1 form and substance on one page.
- Abstract argumentation — which arguments survive attack; the formal cousin of deriving an NPC’s responses from its stances.
- Value-based argumentation — attacks that succeed or fail by what the audience values: the value ranking that settles a conflict.
- Evaluation: later research — earlier notes on judging arguments.
- Glossary — project terms, including the thinking editor.
What links here: Quell story progression: design