Randomized NPC action system - am I on the right path?

Twine Version: 2.3.9
Story Format: SugarCube 2.33.2

(edit: minor rephrasing for sake of clarity)

TL;DR
Im afraid my code for NPC action randomization is unnecessarily complicated and I might not be able to expand/maintain it. Can I do better? How?

I’m using Twine with Sugarcube 2 to create an RPG with a substantial amount of randomized Non Player Character action. I am a newbie to programming/scripting; most probably my goals are way to ambitious and I am in over my head. But I am willing to learn. :slight_smile:

Thanks to examples, discussions and documentation provided by the awesome Twine community I was able to glue some working code together that allows me …

  1. to track where NPCs are and what they are doing. (Only a subset of all NPCs are “active”. I am concerned about the performance as I want to be able to use/generate dozens, maybe even between 100 and 200 randomized in the future, so I to limit the number of NPCs the code has to deal with.)
  2. identify a set of potential actions for each NPC based on the NPC’s class and location. (I want to expand this functionality with action options based on e.g. time schedules, weather, their characteristics, their current action etc. and make use of probabilities for each action option.)
  3. from the set of potential actions for each active NPC randomly choose a next action, including movement to other locations/passages. (I am thinking about giving actions a - partly randomized - duration, allowing interactions between NPCs etc.)

Given my inexperience I am concerned that my approach (basically nesting an awful lot of “if”-macros in a way that looks shady even to myself) may prove too pedestrian and will prevent me from accomplish my ambitious goals.

So I am asking not for help with a specific problem but for your critical feedback: Am I on the right track? Is there an easier way to do what I am doing that might also be better expandable?
As I said, I am only starting to learn. Please bear with me being slow. :blush:

Here is part of the code. I hope it is not too much and not too little information.
I have not included:

Passage “locations” (incomplete, just in here so you get the basic idea):

<<set $innCommonRoom to {
	id: "innCommonRoom",
	region: "town",
	area: "inn",
	name: "commmon room",
	exits: ["innStairwell", "innStable", "innKitchen", "mainStreetLower"],
}>>
[...]
<<set $locations to [$innCommonRoom, $innStable, $innStairwell, $innCellar, $innKitchen, $innFirstFloor, $innGuestroom1, $innGuestroom2, $innGuestroom3, $mainStreetLower, $marketplace, $castleTowerGround, $castleTowerFirst, $harborPier, $shipUpperDeck]>>

Passage "characters"

<<set $pc to {
	id: "pc",
	name: "Dana Sugarcube",
	sex: "female",
	race: "human",
	class: "rogue",
	location: $innCommonRoom,
	area: "inn",
	region: "town",
	destination: $innCommonRoom,
	focus: false,
	currentAction: "",
}>>
<<set $npc1 to {
	id: "npc1",
	name: "Testriel Brightmoon",
	sex: "female",
	race: "elven",
	class: "wizard",
	location: $innCommonRoom,
	area: "inn",
	region: "town",
	destination: $innCommonRoom,
	focus: false,
	currentAction: "sitAtTable",
}>>
[...]
<<set $chars to [$npc1, $npc2, $npc3, $npc4, $npc5]>>

Passage "actions"

<<set $leave to {
	id: "leave",
	description: "leaves",
	standard: true,
	locations: [],
	classes: [],
	probability: 20,
}>>
<<set $sitAtTable to {
	id: "sitAtTable",
	description: "sits at a table.",
	standard: false,
	locations: ["innCommonRoom"],
	classes: [],
	probability: 50,
}>>
[...]
<<set $actions to [$leave, $sitAtTable, $orderBeer, $drinkBeer, $napChair, $napStraw, $smallTalk, $steal]>>

Passage “hereComesTheMess” (merged from several passages that are in the original several passages linked via <>)

<<nobr>>

/* Identify PC's current whereabouts (passage and area) and store them in PC array. */
<<for _i to 0; _i lt $locations.length; _i++>>
	<<if $locations[_i].id == passage()>>
		<<set $pc.area to $locations[_i].area>>
		<<set $pc.location to $locations[_i].id>>
		<<set $pc.locationName to $locations[_i].name>>
	<</if>>
<</for>>
	
/* Reset the presentChars, regionChars and focusChars arrays; identify all characters present at PC's current whereabouts and store them in the presentChars array; identify all characters present in area of PC's current whereabouts and store them in the regionChars array; identify all focused characters and store them in the focusChars array. */
<<set $presentChars = []>>
<<set $regionChars = []>>
<<set $focusChars = []>>
<<for _q to 0; _q lt $chars.length; _q++>>
	<<if $chars[_q].location == passage()>>
		<<set $presentChars.push($chars[_q].id)>>
	<</if>>
	<<if $chars[_q].region == $pc.region>>
		<<set $regionChars.push($chars[_q].id)>>
	<</if>>
	<<if $chars[_q].focus == true>>
		<<set $focusChars.push($chars[_q].id)>>
	<</if>>
<</for>>
/* Combine regionChars array and focusChars array in the new activeChars array which defines all characters that are actively manipulated in the background. */
<<set $activeChars to [].concat($regionChars, $focusChars)>>

/* Display PC's current whereabouts (passage and area). */
I'm in the $pc.area's $pc.locationName.
		
/* Identify and display if the location is private, public, populated or crowded. */
	<<if tags().includes('private')>>
	This is a private place<<else>>This is a public place<</if>><<if tags().includes('populated')>> and there are some people around<<elseif tags().includes('crowded')>>and there are many people around<<else>><</if>>.<br>
	
/* Display all characters present at PC's current whereabouts; based on the present chars being known/unknown to the PC display either name or description. */
<<for _l to 0; _l lt $chars.length; _l++>>
	<<if $chars[_l].location == passage()>>
		<<if $chars[_l].unknown == true>>
		A $chars[_l].sex $chars[_l].race $chars[_l].class is here.
			<<else>><<hovertip "A $chars[_l].sex $chars[_l].race $chars[_l].class.">>$chars[_l].name<</hovertip>> is here.
		<</if>>
	<</if>>
<</for>>
	
/* Display if noone is there. */
<<if $presentChars.length == 0 and not tags().includes('populated', 'crowded')>><<set _alone to true>>I'm alone.
<</if>>
/* For each active char go through the actions list and select an action as available for this char if it is
1) a standard action 
2) a class action matching the char's class 
3) a location actions matching the char's location.
From this array, randomly select one action to be the char's current actions. */

/* Go through all registered characters that are active and reset their possible actions and their probabilities. */
<<for _q1 to 0; _q1 lt $chars.length; _q1++>>
	<<for _q2 to 0; _q2 lt $activeChars.length; _q2++>>
		<<if $chars[_q1].id is $activeChars[_q2]>>	
			<<set $activeCharsActions[_q2] to {
				id: [],
				probability: [],
				}>>
				
/* Go through all registered actions. */
			<<for _q4 to 0; _q4 lt $actions.length; _q4++>>
			
/* Make all standard actions available to the active char. */
				<<if $actions[_q4].standard is true>>
					<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
					<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
				<</if>>
				
/* Make all actions available to the active char that match his char's class. */
				<<for _q5 to 0; _q5 lt $actions[_q4].classes.length; _q5++>>
					<<if $chars[_q1].class is $actions[_q4].classes[_q5]>>
						<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
						<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
					<</if>>
				<</for>>
				
/* Make all actions available to the active char that match his current location. */
				<<for _q6 to 0; _q6 lt $actions[_q4].locations.length; _q6++>>
					<<if $chars[_q1].location is $actions[_q4].locations[_q6]>>
						<<set $activeCharsActions[_q2].id.push($actions[_q4].id)>>
						<<set $activeCharsActions[_q2].probability.push($actions[_q4].probability)>>
					<</if>>
				<</for>>
			<</for>>
			
/* Randomly select one of the char's available actions as the current action. */
			<<set $chars[_q1].currentAction = either($activeCharsActions[_q2].id)>>

/* In case of the "leave" action trigger the "move" widget (see separate passage). */
			<<if $chars[_q1].currentAction == "leave">>
				<<move $chars[_q1]>>
			<</if>>
			
		<</if>>
	<</for>>
<</for>>
<</nobr>>

1 Like

Oof-da. No. This is really bad.

As a quick test, I used your code to generate an array of 15 locations and an array of 100 characters. That’s it. Then I simply looped through a passage 60 times and made 5 save files. At that point, the entire collection of save files took up 230,342 bytes in local storage (about 2.2% of what’s available) and every passage transition took a little over 1.5 seconds, which is waaaay too long. Anything over half a second is perceived as “too long” by most people, and I easily hit three times that length of time. That didn’t even include all of the duplicate copies of the locations and characters that you’re creating, not to mention all of the other story variables.

To prevent that problem, you need to be far more efficient in how you store your data.

First of all, you should try to use temporary variables whenever possible. Since SugarCube clones objects at every passage transition, then doing things like:

<<set $obj1 = { data: "value" }>>
<<set $arr1 = [ $obj1 ]>>

ends up creating two separate copies of that data after the passage transition. One copy is stored in $obj1 and the other is in $arr1[0]. There’s no point in having two different copies of the same data like that. Instead you should do:

<<set _obj1 = { data: "value" }>>
<<set $arr1 = [ _obj1 ]>>

and that will make it so that you only have one copy of the data, because the temporary variable _obj1 is deleted upon the passage transition.

Alternately, if you need to keep the $obj1 variable around, you could do this:

<<set $obj1 = { data: "value" }>>
<<set $arr1 = [ "obj1" ]>>

Now you’d also only have one copy of the data, which would be inside the $obj1 variable, and you could access that data from the array by using State.variables[$arr1[0]].

Another optimization would be to store any data which never changes on the SugarCube setup object. In your JavaScript section or your StoryInit passage you’d create that data, and then basically treat the data as read-only for the rest of the game. For example, in your StoryInit passage you might do this:

<<set setup.locations = {}>>
<<set setup.locations.innCommonRoom = {
	id: "innCommonRoom",
	region: "town",
	area: "inn",
	name: "commmon room",
	exits: ["innStairwell", "innStable", "innKitchen", "mainStreetLower"],
}>>

and rather than storing a copy of that entire object within an NPC’s object, you’d simply store the string “innCommonRoom”, and you could then reference that data using setup.locations[$chars[_charID].location].

That way you have access to all of the location data, but none of it is wasting space with multiple copies of the same data being stored multiple times throughout the game’s history.

Also, the idea of having and tracking hundreds of NPCs is ridiculous. Can you think of any game you’ve ever played where you had to remember hundreds of NPCs? And if you can, do you actually remember more than a handful of them?

NPCs should be generated on an as-needed basis, and disposed of if the player doesn’t interact with them. You should only track the handful the player actually seems interested in, since otherwise you’re just going to overwhelm them.

Also, not only should NPCs be generated on an as-needed basis, but details about randomly generated NPCs should be generated on an as-needed basis as well. If the player never finds out their name, then you don’t need to give them one. You only need to create that data at the point where you actually need it.

Thus you’ll want to create a widget, macro, or function to get NPC data, and if the data you request isn’t already generated, then it will generate that data at that point.

Furthermore, once the player is done with an NPC that they’ve interacted with, that NPC’s data should be deleted.

Basically, you want to structure you data in a way so that as little data as possible is stored in story variables, otherwise your game is going to get extremely laggy extremely quickly.

You might want to take a look at my Universal Inventory System (UInv) for storing data like this. UInv is designed as an inventory system for Twine/SugarCube which minimizes the amount of data that gets stored in the game’s history by referencing default “item templates” you set up in the JavaScript code. It then only stores references to the templates, plus any modifications to those templates, in the game’s history.

You could use UInv, not just for storing data about inventory items, but also information about the PC, NPCs, locations, and other things, and it should help you do all of that more efficiently.

Hope that helps! :grinning:

2 Likes

:disappointed_relieved:
Oh, no! And I was sooo proud that this code seemed to do the stuff I expected it to do.
Seems like I went full “Sorcerer’s Apprentice” in the process and created a monster - more precisely, an army of them.:rofl:

Thanks for the heads-up, @HiEv! That was exactly what I needed (although not wanted) to hear. So your feedback is very appreciated and helps a lot.

I will need to go back to my concept and change my approach. I will definitively check out the UInv and also try to improve my understanding of concepts like temporary and story variables, widgets etc.

2 Likes

I agree with HiEv in that your approach is overly ambitious. I also learned Twine on the go, creating some pretty big projects. One of them included lots of procedural generation, but wasn’t nearly as ambitious as tracking hundreds of NCPs! And my code was a mess anyway :wink: I’m only now learning how to make it more efficient and transparent.

So, I believe your original approach would mean putting a ton of effort into something most players wouldn’t notice or appreciate. Adding a ton of individual NPCs isn’t required to create the feeling of a populated world. Most big cRPGs get away with having a relatively low number of fleshed out NPCs and a lot of unnamed background characters.

I think my approach would be to create a few unique characters and randomize them upon game start: assign names, classes, battle cries, favourite foods, political views and whatnot. I wouldn’t try to track their actions/movements, instead assigned them to locations based on some variable, like the time of day. So, for example, NPC A always spends mornings and evenings in the inn, and afternoons in the town square – unless they get killed when the player makes a certain choice in the story. When you visit the inn, you only need to check if it’s morning or evening now, and if NPC A is still alive; if so, the game mentions them in the passage (if you want to add some more flavour, you could include an “inn activities” (“town square activities”, …) property in your NPC objects; it would include several descriptions, one of which is printed at random each time the game describes NPC A in the inn. For example, if NPC A is Kevan the Dwarf, he would be drinking beer or playing with his knife, while Gerta the Rogue would be drinking wine or playing cards). To enhance the illusion of NPC activity, some invisible, random modifiers could be applied as well (for example, a variable could be set to 1 or 2 at the start of each new day; when it’s 1, NPC B spends the evening in the inn, but when it’s 2, they’re absent, likely on some business or at home). It’s all relatively simple, but the more possible activities and combinations you add, the more complex your game will get, so you’d have to decide on an adequate level of complexity: one that doesn’t make your world and characters seem static, but on the other hand isn’t overwhelmingly complex while adding little to the player experience.

I’d also use a very simple trick to pretend there are dozens of background NPCs populating the game world: write some setup objects containing arrays with lots of brief descriptions of people you can meet (“a grumpy gnome”, “a smiling gravedigger”, “an elderly washerwoman”, “an elf hunter”, etc.); then each time you visit a location, one or two of these are inserted at random into the location description. You could use separate arrays for different areas of course, so there are more adventurers in the inn or in the potion shop, and more common townsfolk in the town square or market. And if the descriptions are sometimes repeated (not in the same passage, of course, you’d have to prevent that somehow), it’s not a problem – it creates the illusion of these background NPCs actually existing in the game world and crossing their paths with the PC multiple times. I, as a player, would be completely fine with having characters like that mentioned by the game, without them being interactive. (Or, you could add the option of asking some general questions to “the people” in the location, e.g. “Ask about the Duke” → “The people in the town square tell you their lord the Duke has been acting strangely of late”).

So, these are my design thoughts :slight_smile: Hope you’ll find some of these useful. Good luck with your game! I’d be really interested to see how it turns out and what approach you’ll take.

1 Like

I like this approach and all your following ideas a lot, Agnieszka. I can see how this might create the illusion of a living place much better than the lists of automated NPCs which I would have ended with.

I also see now that I didn’t start small enough, ignoring HiEv’s advise on “Game Making for Dummies”. I was basically following a grand scheme of creating sandbox game mechanics that (even if it had worked) most probably would have been uneconomical anyway for what the player will experience and appreciate in the end. And I am not prepared yet for the technical rabbit hole that it is leading me down.

Thank you, Agnieszka, for sharing your experience! Your input makes me think a lot more about what I want to accomplish.

I am ready to shift my focus now on the skeleton of a story, interesting NPCs and a lively setting. Fleshing out the first idea of fun gameplay. And only then decide where to add bells and whistles.

1 Like

Sooo. I had a look at UInv.
Now I’m feeling like I was handed a … bag of holding. Which itself is filled with bags of holding. :sweat_smile:

  1. Following your advice, I could now set up locations and NPCs (which I both want to be able to manipulate during the game) as default bags in the UInv JavaScript, correct?
  2. And then how should I continue?
    a) Set up each predefined location and each of my (handful :slight_smile: ) predefined NPCs as UINV default items? And, the moment the PC meets this NPC, give the NPC item a property referencing the location item via SetItemPropertyValue?
    b) Or should each location (like “innCommonRoom”) be a bag itself? And I move an encountered NPC from the NPCs bag to this other bag “innCommonRoom”? Does that make any sense?
  3. What about stuff like character classes, races or spells which I think of being immutable? You proposed putting these to the setup object. Does that still hold up if I use the UInv? Or could I use also bags/items for them to be consistent?

This is exciting! I hope you don’t mind the questions. :blush:

Correct.

Well, that depends on how you want to think of things. There are lots of different ways to do this, so you have to figure out a structure that works best for you.

I’d probably make both locations and NPCs as “bags”, and give them all “type” property using “tags” to indicate what type(s) they are. On NPCs you’d have a “location” property to indicate what location they’re in (by location “bag” name). Any actual items would then be placed inside the appropriate location or NPC “bag”.

It kind of depends on what you’re doing with them, but either way it shouldn’t make much difference. They’ll probably be a bit easier to work with on the setup object, though if you add them to UInv then you’d be able to use the UInv functions on them. The important thing is that they’re not stored in Story Variables.

If you use the setup object you could just set properties on the NPC “bags” to track their class, race, and spell list. You can then use that data to access the rest of the data for those things from their setup objects.

If you use UInv, then you could make a “details” property on the NPC “bags”, which is a “pocket” that contains the “class”, “race”, and “spell” “items”.

Hope that helps! :grinning:

1 Like