Formal(-ish) Models For NPC Decision-Making and Persuasion

Continuing the discussion from Creating a persuasion-based Detective story with AI elements in dialogue:

So to give some framing, I’ll sketch out the outlines of the relevant bits of the game design I’m working on (because it informs how I’m framing NPC decision-making).

In terms of genre, I guess you’d call it non-Lovecraftian cosmic horror. One of the central ideas, in terms of things I wanted to work on and why I’m writing it as an IF game, is that all of the major characters are dealing with regret. Specifically regret over the things they haven’t done, choices they didn’t make. The road not traveled, that kind of thing.

The game is set in a small town (a formerly mildly prosperous rural tourist trap that’s well past its prime). At the start of the game, all the NPCs have set “agendas”, which basically just means they each have their own routine: get up in the morning, go to work, that kind of thing. Think of Majora’s Mask or, more recently, something like The Outer Wilds. As the game’s narrative advances, conditions get progressively worse, and one of the things I want to model is the slow degradation of these normal behaviors. I don’t want it to be just scripted intervals: player does foo, that triggers a scripting flag and boom all the shops in town are closed.

Another core game design decision is that I want almost all puzzles and puzzle-like activities in the game to have one or more NPCs that can help the player solve them. Either by coaching (giving hints in conversation), or (at the player’s prompting) to just solve them outright. Basically as a guard against the player running up against a kind of puzzle they bounce off of and having their overall advancement blocked.

Here I’ll use made-up gameplay situations using a UFO crash landing and a zombie outbreak. That’s not what the game’s about, but it’ll be easier to talk about things in concrete terms, instead of making veiled references to actual gameplay situations.

So at some point in the game if the UFO has crashed outside of town, zombies are roaming the streets, and the player needs help from a biologist, say, at the crash site. But the biologist has boarded themselves up in their home and won’t leave.

I think the “traditional” approach to this sort of thing is to basically have scripting that encodes the stuff the biologist specifically cares about, and so “recruiting the biologist” becomes either something like a fetch quest (obtain the equipment from the overrun lab and return to the NPC) or a fill-the-affection-meter task (do a bunch of little things for the NPC, each of which increments a counter which eventually crosses some deterministic threshold past which they become an ally).

The approach I’m taking, which I touched on in the other thread, is instead to model each NPC’s overall drives and mental state, and then frame each decision in terms of what satisfies those needs and desires, and have NPCs make decisions based on their evaluation of what is currently most compatible with their overall wishes.

In terms of authoring interface, NPCs are built using a sort of “personality quiz”, loosely modeled after the one used in Ultima IV. Except instead of selecting a best match from a set of pre-defined archetypes, the quiz questions each apply adjustments to a nominal baseline model, making the NPC more sensitive or less sensitive to certain kinds of stimuli, as well as tuning their baseline mental state. Example:

Blockquote

Question 1

You’ve boarded up the windows, blocked the doors, and now you and the other survivors, hungry and tired, are huddled in the darkness listening to the howling things outside.

A: Quietly gather some supplies for yourself and hide the remainder.
B: Wait for dawn and then slip out for a resupply run without telling anyone.
C: Huddle with the rest and wait for whatever is coming.
D: Start directing the others, making sure they recognize you’re in charge.

HELP or ? for help.

>

The kinds of adjustments should be fairly apparent (more greedy versus more risk tolerant and so on) and to be clear it’s an authoring tool rather than a “real” personality test. The main goal is to just mean that I don’t have to hand-write the numerical values (discussed below) directly.

There are also bits for defining what sorts of inhibitions, if any, the NPC has (specifically for allowing disinhibition to affect decision-making in some circumstances) as well as what kinds of impulsiveness, if any, the NPC is prone toward (generally things like is the NPC more likely to act impulsively if they’re felling good or when they’re felling bad, when they act impulsively does that just amplify their normal behavior or are they likely to do things they wouldn’t otherwise do, and so on).

What that produces is a baseline mental state, which is a vector (just a list of numbers) over a set of pre-defined mental channels: fear, anxiety, fatigue, pain, anger, shame, social isolation, and so on. Because this is a horror game, all of the channels are “negative”. That is, a large positive numeric value is “bad”, and generally positive mental states are negative numeric values. Everything is done using integer math because this is all in TADS and floating point in TADS is slow.

At this point I’m going to start using some jargon, and I want to give an explicit disclaimer. I’m a programmer who read a bunch of papers on psychology to gather material for writing a video game. I’m going to use a lot of psychological terms, but most of them are being used in a slightly different sense than they are in the literature, and many of then are used very differently.

With that in mind, the vector described above is called the “disposition”. It is the durable baseline the NPC tends toward. There’s also a mutable vector, the “activation”, which gets tweaked by interactions with other characters and the environment. In the normal course of things the activation regresses by itself to the disposition. At any given point, most decision-making uses a mental state vector that’s the mean of the activation and the disposition.

The mental state represents how important that channel is to the NPC. So a high fatigue value doesn’t mean the NPC is currently tired, it means that they react strongly to being tired.

There’s also a vector, again with exactly the same channels, for physical drives. These represent how [whatever] the NPC currently is. Some of these are tweaked monotonically by passing time (like hunger and fatigue) and are automatically reduced by in-game interactions (like eating and sleeping), while others are tweaked by feedback from decisions, by scripted events, and so on.

The drive and mental state together form, and sorry for more jargon, the desire pre-image. We’ll get to the desire vector in a bit, but it’s defined in terms of what things provide, called affordances. Affordances are things like comfort, social connection, and so on. The desire pre-image isn’t in those terms, it’s just the net mental state. By default it is computed using the drive vector as a scaling factor that grows the further the drive vector channel deviates from the baseline. That is, a small amount of hunger shapes decision making a small amount, a large amount of hunger shapes decision-making more.

To translate between the mental space and the above-mentioned affordance space each NPC has an embedding matrix. Each cell in the matrix corresponds to the combination of one mental channel and one affordance channel. The semantic meaning of each cell is “how much does this mental channel’s value change how much the NPC values what this affordance channel offers”. The desire vector is just the product of the desire pre-image vector and the embedding matrix. It can be read as how much the NPC currently cares about each affordance, as a result of their overall mental state and physical drives.

The simple version of decision-making is then to just treat the values of the desire vector as relative probabilities and use them to pick one (norm them so they sum to 1000, pick a number between 1 and 1000, figure out which option the number corresponds to), and there’s your decision.

But that decision would just tell you which affordance they most want satisfied. For choosing between different in-game options, each query starts out with a list of options, each option having an affordance space vector representing what it offers. That is, if you pick this option, these are the affordances (and weights) you’d get.

So now the simple model is to just compute the dot product of each option with desire vector, which results in a scalar (an integer) representing, roughly how strongly, overall, the NPC likes that option. The values for each option are then treated as relative probabilities (as described above), and an option picked based on that.

Of course there are additional complications. What is described above is acting directly on the result of projecting the net mental state through the embedding matrix, the raw desire vector.

But in addition to just computing the raw desire vector, there’s a (very rudimentary) feedforward network(-ish thing) that consists of a series of ordered layers, each of which takes the current desire vector as its input and can process it in various ways to produce an output vector which is then fed into the next layer. And the net desire produced by all the active layers combined produces the final desire vector passed to the sampler (the thing that rolls the dice to make the choice).

The layers handle things like feedback-based learning (developing “preferences” bases on the outcome of choices based on their actual reward, in affordance terms), modelling regret (NPC taking note of what they didn’t get as a result of making choice A instead of choice B, perhaps making them more likely to choose B in the future), adjusting the desire vector based on the NPC’s relationship with other characters if the option is a request from another character or if it involves another character, as well as some fancy stuff involving altered mental states that could affect decision-making (trauma resulting in paranoia, which then places ceilings and floors on some channels until a timeout or external trigger resets the state, and that kind of thing).

Right. That’s the basic mechanics. The main other component is a query builder for easier authoring of query options. As mentioned above, each option involves an affordance vector, representing what the choice offers (or rather what the NPC believes the choice offers).

But writing a bunch of math for every query is messy and error-prone, so instead most queries are built from a library of stock query option types. This covers generic things like agreeing or disagreeing with a general request based on who is asking, to situations like exploring a potentially dangerous situation versus going to ask for help, to confronting someone versus being evasive. These can be scaled, with the idea that, from an authoring perspective, they look something like an encounter in a board game with a bunch of skill checks: explore the creepy library after dark might be an “explore a dangerous place” task with a difficulty of 1, while exploring the ancient temple crawling with torch-bearing acolytes of the Dark God would be a “explore a dangerous place” task with a difficulty of 5.

Similarly, there’s an “appraisal” system, where NPCs “see” tags on objects sorta like they’re item cards in something like a board game: this is a “tool item, utility 1” versus this is a “quest item, utility 3” and so on, with per-NPC tuning values automagically converting items tagged in gameplay terms into affordance terms evaluable by the huge mess described above.

Anyway. That all sounds pretty heavyweight. And I’ve spent a lot of time hammering on it (and it isn’t done yet). But on a turn-by-turn basis even in the fairly sluggish TADS3 VM, on an ~10 year old laptop individual decisions (with an unrealistically large number of bells and whistles enabled for testing purposes) still only take on the order of ~100 μs. So even in my comically overdesigned worst case test scenario (400 NPCs navigating a 400 room maze) the decision logic still takes less time than T3 takes just updating the location of 400 objects.

That’s a lot of text. I’m willing to talk more about details if anyone’s interested. I’ll eventually make the T3 code available like almost all the other modules I’ve developed for this project, but I’m planning on keeping the NPC decision engine and the procedural map generation stuff private until after release, whenever that is. Don’t mind discussing the details, though.

5 Likes

I find this all pretty fascinating. I’m intrigued to eventually play your game. I’m working on a long-term parser project and early in development I had the idea to design a conversational system around this concept of the emotional color wheel which would allow for tracking an NPC’s emotional state using RGB values, which I imagined would allow for fairly complex decision making using relatively simple math, but I have not yet got around to that piece.

Your NPC system sounds like something that could be widely applicable, and I wonder how portable it might be. Do you think it will translate outside of TADS?

1 Like

This sounds like a really interesting system! The main thing I wonder, though, is—how transparent will it be to the player? How well will a player understand what they’re doing, and why certain actions led to certain outcomes? How well can the text convey the subtleties of what’s going on behind the scenes so that the player perceives it as something they can learn about and interact with, rather than just “people have a random chance of saying yes or no”?

1 Like

The color wheel model comes from a 1980 paper by Robert Plutchik, “A General Psychoevolutionary Theory of Emotion”. It is based on, in part, Russell and Mehrabian’s PAD model, the common citation being the 1977 paper “Evidence For a Three-Factor Theory of Emotions”. The M&R paper, and Russell’s later circumplex model are very much sources for the ideas I used in writing the code.

One of the results M&R gives is of a study in which a bunch of undergrads were given a list of emotional terms and were asked to rate them in terms of pleasure (that is, when you are experiencing this emotion, does it feel good or bad; this is sometimes also called “valence” in the literature, as in “positive valence” versus “negative valence”), arousal (sometimes “activation”, the distinction being made here is whether the emotion is associated with a high or low energy level: getting to sleeping late and winning the big game are both positive valence but at opposite ends of the arousal scale), and dominance (whether the emotion is associated with feeling in control or feeling out of control). The result was that the values are in aggregate fairly consistent, and more or less map out a continuous space. The weakest correlations were found for the “dominance” axis, and the later circumplex model is basically just a refinement of just the P and A elements from the original M&R model.

Anyway, the advantage, from my perspective, is that they give their results numerically, basically as population mean and standard deviations, which makes them very amenable to incorporation in a computational model like the one I’m building. I will add a substantial caveat that I basically just treat mental state vectors as just defining a point in a problem space, and evaluate similarity between different emotional states as Euclidean distances and this is not what the circumplex model presents: the metric it offers is radial separation between points on a circle.

That said, what I use it for (apart from assigning initial SWAG values to some things) is affective evaluation of NPC mental state. That is, if you want to script some NPC behavior and want to do different scripted things (like displaying different flavor text for a fidget, or forcing different choices in big narratively-important plot inflection points) by keying off whether the NPC is currently bored, exuberant, tranquil, anxious, or whatever, just seeing which indexed mental state the NPC is closest to is pretty straightforward and seems to be “intuitively correct” most of the time, even if it’s extrapolating outside of the supportable claims made in the paper. And while I’ve been generally relying on “nearest indexed affect” just because I think it’s more authorable, under the hood the model is always computing a mathematical value for the PAD evaluation, and it can be used directly instead.

Example of a random run of a debugging test case, to give a feel for the knobs on the programming interface:

disposition eval:
   best disposition match:
       Calm (910)
   disposition matches by type:
       social: Calm (910)
       pathological: Stoic (680)
       positive: Confident (852)
       neutral: Nostalgic (659)
       negative: Sad (674)
   top three disposition matches:
       Calm (910)
       Confident (852)
       Contempt (836)
affect eval:
   valence: 410
   arousal: -321
   dominance: 405
   affect: relaxed

The lattermost bit is just exactly a PAD-ish evaluation based on the current mental state, considered as a point in the PAD space (values in [ -1000, 1000 ], where 1000 is the general integer scale factor throughout).

The upper bits are comparing the current mental state to items in a library of pre-defined disposition vectors, grouped by type. This is used both for evaluating the NPC mental state, and for scripting that shifts the NPC’s mental state in the direction of a named disposition type. Basically just a method that takes a type ID and an integer gain and lerps between the current mental state and the target disposition.

In theory all of the actual decision logic is absolutely portable. In fact it would have been enormously easier to implement all of this in python or javascript or literally anything with a even moderately performant floating point implementation.

That said, several years ago when I started working on this project I looked specifically at Inform first. At the time my design test cases were a poker hand evaluator and a very simple form of a decision engine that just handled fold, raise, and call decisions for NPC poker-playing AI. And I couldn’t figure out how to do it in Inform, and it was comparatively straightfoward in TADS3, so TADS3 is what I went with.

That could just be due to a lack of experience with Inform, though.

Beyond the bare decision logic there’s a bunch of other code I wrote for TADS3, like the targetEngine module and the memoryEngine module, and the code I currently have in the unreleased decisionEngine module leans on them to actually produce behaviors. So that stuff isn’t really portable, but it’s a fairly small proportion of the overall code.

I also relay heavily on a dataTypes module that contains most of the linear algebra stuff (matrix and vector math) that I use in the decision logic. The decision engine and the procgen map creation stuff are probably about 75% of the code in the dataTypes module.

1 Like

For that stuff specifically a lot of it is down to the writing of the interactions themselves. My basic model is that most early interactions are comparatively free-form and low-stakes, and from the start the core gameplay loop will emphasize that the player should be thinking about things as iterative choices.

Time works kinda-sorta vaguely like a modern Persona game, if that means anything to you. Days are divided into segments: early morning, afternoon, evening, and so on (via calendar cycles from the calendar module). Time doesn’t advance automatically, but only when doing certain tasks (like “dungeon” exploration, which in this case means things like “searching the woods for the UFO crash site”; or “searching for a copy of the Necronomicon at the local library”). And the player will learn early on that they’re not playing out a single day or just a couple days, but they’re going to be thinking in terms of “what kinds of things can I do in afternoons” and “what kind of things can I do at nights” and so on.

So in addition to scripted hinting in the responses (“Nah, I can’t get a beer after work, I’ve got too much other stuff to take care of” or “Nah, I’m just not feeling it today” or whatever), there are a couple specific NPC interactions that are intended to basically offer a tutorial on the mechanism(s), in that the player will just naturally end up interacting with them every day. And then hopefully translate the pattern into interactions with the rest of the NPCs.

I also have a dig-as-much-as-you-want, fourth-wall-breaking in-game tutorial mechanism: bathrooms in the game always contain hints for the nearby puzzles and basic gameplay mechanics. And the bathroom near the first NPC you meet has information on how NPC interactions work. Or rather it will when the game is close enough to being done that I actually start replacing all the placeholder text with actual gameplay and puzzle hints.

That all applies to the specific case of player-NPC interactions. Which is a big part of the design. But the point I’ll make is that the decision-making logic is basically just a (hopefully) nuanced way to roll the dice. Part of that is giving flavor to player-NPC interactions, but those interactions are never (in my design) going to be purely procgen things. That is, I’m not using an LLM (or anything else) to generate text, so the basic writing task isn’t that different from what you’d do in scripting the interactions using a more “traditional” affection gauge/fetch quest kind of thing. That is, you’re still having to add hinting (either directly in dialog or off to the side, environmentally, via interactions with other characters, or whatever) to clue the player in to whatever the blocker is.

And then the other part of the design, which is NPC decisions that aren’t part of direct interactions with the player and I think those end up reading as “working” even if the player can’t directly see or deduce the inner logic that produced the behavior. Like, if instead of just standing around and being a dialog kiosk some of the NPCs are doing things independent of the player’s actions, that is, or at least can be, inherently interesting.

albeit for too well known reasons (US’s filthy mouth targeting MY country…) I’m again in “TBD” timeframe on IF matters, is well-known that I’m seriously working around NPC’s emotional models and PC/NPC dynamics, so obviously I’m following this discussion (and taking notes… :wink: )

I suspect that the “emotional color wheel” model can be easily set up (albeit in “monochrome”, so to speak), in TADS3/A3Lite using the new mood and stance extensions; I’ll look if and how the “gray scale” of the mood/stance extension can be turned into “RGB channels” but no promises on reporting time…

The assignment of NPC initial state is, in my case, the main narrative issue: the PC’s “first contact”'s actions sets the initial state but the devil’s (and angel’s) detail is in the rather continental (where PDA is the norm) nature of said 1st contact.

On NPC behaviour, you cited also fidgeting, and I must note that where I live body language IS a major part of the language, that is, here a fidgeting change the meaning of the spoken words. so isn’t a minor point in my narrative. (astute people whose have played First Contact will have noticed some details…) so, in due time, (BEWARE: major spoiler) keep an eye also on Azuj’s Taily and Miyai’s wings

Viva l’ Italia !
dott. Piergiorgio.

Ah, I meant “fidget” in the gamedev sense of an idle activity the NPC does that’s different from their normal idle behavior.

In T3, an NPC that’s idling will just be reported in the room description with something like “Bob is here.” It’s pretty common to just output some random flavor text every couple turns just to make the NPC appear more lifelike. Some of those will be fidgets in the “normal” sense of the word…“Bob clears his throat and shuffles his feet.” and so on. But something like, “The shopkeeper sweeps the floor, angrily muttering under their breath.” would also qualify as a “fidget” in the gamedev sense, although most people wouldn’t call something like sweeping the floor “fidgeting” in the usual conversational sense of the term.

Anyway, what I’m talking about is having inventories of emotional affect-specific idle activities like this. Just flavor text, but (hopefully) telegraphing to the player information about the various characters’ mental states, without having to directly script all of it, or declare it explicitly in exposition.

I like the idea of dice-rolling as a mechanic, say player rolls d100 against a success rate of 45. If the player seeks advice, perhaps the roll adds another d20 to the roll for each “piece” of advice they successfully sought or each character they consulted with. Seeking help allows assistive ‘co-rolling’.

which is exactly what I’m designing. The issue lies in players’s tendency to overlook flavor text. Example, if a story-changing storm is telegraphed by an apparent flavor text like, “some leaves fly past you, carried by a southeastern wind” will be considered unfair, isn’t ?

Best regards from Italy,
dott. Piergiorgio.

Familiar with the 3DS title Crimson Shroud? It’s a fantasy RPG by Yasumi Matsuno (Ogre Battle, Final Fantasy Tactics) that is explicitly presented as a tabletop experience: there’s a narrative and dialog and so on, but the characters and monsters are depicted as posed figurines (with rectangular bases) that are placed in a little diorama. Die rolling, using various D&D-ish polygonal dice, is explicitly modeled. Both in the sense “here you are not just picking a random number but are rolling a d20”, and “you are using the stylus to model rolling physical dice into a physical space, using a physics model, and you can e.g. bounce the dice off the table, with gameplay consequences”. A lot of the strategy involves doing things like building combos to gain bonus dice that can be added to things like skill rolls (or initiative rolls, savings throws, and so on), more or less like you’re describing.

I wasn’t thinking about doing anything quite like that in my WIP, one of the main mechanics is just bare iteration: trying again later. Think of how you approach a single run in Blue Prince, or something like that. There’s an RNG element. But you have some tools for managing the randomness, and slowly gain more as you go. But also a lot of long-term strategy isn’t about forcing a specific result on a particular run, but figuring out all of the other avenues open to you if you “fail” a specific die roll.

One of the specific things, not directly related to getting a particular response out of a given NPC but more generally, is that for things like “dungeon” exploration, the game uses seeded PRNG to generate things like random maps and resource placement. And the player has a notebook in which they are (automagically) recording the particulars of each day’s explorations…which allows them at the start of each outing to decide if they want to explore a new region (allowing for the possibility of finding new kinds of things) or returning to a previously-visited one with a known layout.

The basic mechanic for this I’m currently working with is that types of things remain constant across multiple visits (that is, they are seeded only by the location seed) while others vary each time (seeded by both the location and the day seed). So a “dungeon” generated for exploring the woods might happen to have a “clearing in the woods” type room in a particular location. Each “clearing in the woods” location has a chance of generating a number of random events specific to being a clearing in the woods. If you revisit a location that had a clearing in the woods last time, it’ll still have a clearing in the woods, but the event you get out of visiting it will be randomly selected every time.

So if you’ve learned that there’s a clearing in the woods where people have sighted UFOs at nights, then every night when you set out you can either revisit a known location where you know you’ll get a random “clearing in the woods” event but won’t encounter certain other kinds of events…or you can try taking a new path, which might produce one of the new kinds of events you’re trying to get, but which also might not have a clearing in the woods room.

In any case, each run is designed to be relatively short and will always produce something. So even if the player is angling for a specific result and don’t get it a) there’s always the next run, and b) the amount of “wasted” effort isn’t, like, “game is now in a zombie state, must reload from an earlier save”, it’s just you spent a couple minutes on a run that gave you foo instead of the bar you were hoping for.

That’s actually one of the reasons why one of the first things I built was poker-playing. Because the idea that you’re trying to model the other player’s mental state through involuntary tells is baked in.

1 Like