If you followed this tutorial series from the beginning, you should now have a good idea on how to use Twine with the Harlowe story format. You’ve created a virtual campground, allowing you to explore all the various sites. You can find and pick up objects. In some cases, you can even use those objects.
All that’s missing is your story’s antagonist, Bernie. Bernie is your Twine NPC who will search the campground for the player, and should he catch them, he will force them to eat his chili. The player can refuse the first, but for the second time, it’s game over.
Designing your NPC
You may be tempted to jump into your code and start typing away. There’s an old saying in development circles: “Weeks of coding can save you hours of planning”. In this light, it’s better to understand Bernie’s requirements before trying to code him into the story.
First, Bernie will wander throughout the campground on a random route. The only real requirement is that he does not backtrack unless he absolutely must do so. There are places where Bernie cannot go. He can’t walk through the mess hall or wander down the dark path near the camp waterfront. This provides some escape routes for the player.
Bernie is not intelligent. He will move to a location, and then randomly pick an adjacent location. He will continue to do this until the player acquires all the story objects or if he moves into the player’s location.
Second, Bernie needs to warn the player that he is nearby. If he is adjacent to the player, the player will hear Bernie’s footsteps and whistling. This gives the player time to move and plan accordingly. Unfortunately, there is an edge case where the player might encounter Bernie without a warning.

Here the player is in location A and Bernie is in Location C. Since Bernie isn’t in Location D or in location B, the player will not hear Bernie’s whistle. The player thinks they are safe so they move to Location B. Bernie also moves to Location B.
While this is a “fair” move, the player wasn’t warned. They will “feel” like Bernie is cheating even though it was a “valid move”. If Bernie collides with a player and the player wasn’t warned, , then Bernie will “stand still” at his original location. You’ll do this in the next tutorial.
Setting up Bernie
To get started, open up your story in progress, or you can download the starter project here:
In this story, you created a campground map for the play area. To Twine, your story is just a series of connected passages. There is no game world. You’ve defined your locations in code. In order for Bernie to traverse the map, you need to define the exits for all the locations. You’ve already defined the rooms and assigned internal ID numbers.

Open the Setup passage. Update the $rooms datamap to the following:
(set: $rooms to
(a:
(dm: "name", "Camp Entrance", "id", 1, "bernieExits", (a:2,13,11), "exits", (a:2,13,11)),
(dm: "name", "Side Road", "id", 2, "bernieExits", (a:1,3), "exits", (a:1,3)),
(dm: "name", "Waterfront", "id", 3, "bernieExits", (a:2,4), "exits", (a:2,4,15)),
(dm: "name", "Pondside Road", "id", 4, "bernieExits", (a:3,5), "exits", (a:3,5)),
(dm: "name", "Crafts Station", "id", 5, "bernieExits", (a:4,6), "exits", (a:4,6)),
(dm: "name", "Intersection", "id", 6, "bernieExits", (a:5,8,13), "exits", (a:5,8,13)),
(dm: "name", "Store", "id", 7, "bernieExits", (a:8), "exits", (a:8)),
(dm: "name", "Parade Ground", "id", 8, "bernieExits", (a:7,6), "exits", (a:7,6,9)),
(dm: "name", "Mess Hall", "id", 9, "bernieExits", (a:), "exits", (a:10)),
(dm: "name", "Facilities", "id", 10, "bernieExits", (a:11), "exits", (a:11)),
(dm: "name", "Back Road", "id", 11, , "bernieExits", (a:1,10,12), "exits", (a:1,10,12)),
(dm: "name", "Hopi Campground", "id", 12, "bernieExits", (a:11,13), "exits", (a:11,13)),
(dm: "name", "Dusty Road", "id", 13, "bernieExits", (a:1,6,12,14), "exits", (a:1,6,12,14)),
(dm: "name", "Tombstone Campground", "id", 14, "bernieExits", (a:13), "exits", (a:13,15)),
(dm: "name", "Camp Path", "id", 15, "bernieExits", (a:), "exits", (a:14,3))
)
)
Here you’ve defined two properties that contain array. benieExits represents all the possible exit that Bernie can use. The numbers refers to the room IDs. Notice, the Mess Hall contains any empty array. This means there are no available exits for Bernie.
The exits property represents all the location exits. These are the exits that the player can use. This property is useful to know to determine if Bernie is nearby.
Now you need to define some support variables. Open the Setup passage. Add the following in the passage. Keep in mind that the variable’s location doesn’t matter in this passage but it’s a good practice to group all your variables together:
(set: $bernieStartingLocations to (a: 3, 4, 5, 6, 8, 10, 12, 14))
(set: $bernieLocation to (either: ...$bernieStartingLocations))
(set: $previousBernieLocation to -1)
The bernieStartingLocations are all the potential starting locations for Bernie. In the worse case situation, the player at least be able to make one move before encountering Bernie. As a an exercise, you may to give the player several turns before “unleashing” Bernie. Consider that coding challenge to do on your own.
The bernieLocation property is the randomly determined starting location. This uses the (either :) macro. The (either :) macro takes a random value from a “spread” of values. Think of a spread of cards being laid out on the table. You spread out the values of an array but using the … operator.
Finally, the $previousBernieLocation keeps track of Bernie’s previous location. This represents a room’s ID. Since Bernie hasn’t moved, it is set to -1 at the start of the game.
Setting Bernie’s location
Now comes the fun part. Bernie is the real heart of the story. The logic is quite simple, but you can make it as complex as you would like. For instance, you may want to add tracking logic so that Bernie will follow the player around the map.
For now, create a new passage and call it Move Bernie. Add this code:
(set: $newLocation to -1)
This represents Bernie’s new location. The -1 value means that there is no location. It’s just a placeholder value. Now add the following:
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
]
]
This loop cycles through all the existing rooms to determine Bernie’s current room. Add the following inside the (if :) statement:
(set: $availableRooms to _room's bernieExits)
This gets all the available locations to Bernie. Add the following:
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
]
]
]
<!-- replace me //-->
First you check to see if there are multiple exits from the current room. Then, you randomize the order of the exits. This randomization is Bernie’s “logic” for selecting a new room. By shuffling the exits, this ensures that Bernie will choose a random walking pattern. The last bit of code checks to see if a new location was selected.
You’ll notice the <!-- replace me //-->
statement. This is known as a comment. You can put anything between the <!--
and the //-->
and Twine will ignore it. This is useful to leave notes about your code. You’ll replace this comment momentarily. Think of it as a placeholder for now.
Add the following inside the current (if :) macro:
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
This logic checks to see if Bernie has recently been to a location. If he hasn’t, then he’ll take that exit. Here’s the completed code:
(set: $newLocation to -1)
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
(set: $availableRooms to _room's bernieExits)
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
]
]
]
<!-- replace me //-->
]
]
There is a very good chance that Bernie might not have a new location set for him. Replace the <!-- replace me //-->
comment with the following:
(if: $newLocation is -1) [
(set: $newLocation to $availableRooms's first)
]
This is a safety measure. Prior to this code, there may be situations where Bernie will not be able to move. For example, if Bernie moves to the Camp Store, he’ll be stuck there since he’s “coded” not to back track. This code at least provides a way for Bernie to move.
Here is the completed code:
(set: $newLocation to -1)
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
(set: $availableRooms to _room's bernieExits)
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
]
]
]
(if: $newLocation is -1) [
(set: $newLocation to $availableRooms's first)
]
]
]
Unfortunately, this code will great a spacing nightmare. Make sure to wrap it in braces to remove all extra space.
{(set: $newLocation to -1)
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
(set: $availableRooms to _room's bernieExits)
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
]
]
]
(if: $newLocation is -1) [
(set: $newLocation to $availableRooms's first)
]
]
]
}
Finally, add the following to set Bernie’s new location before the last closing white space brace.
(set: $previousBernieLocation to $bernieLocation)
(set: $bernieLocation to $newLocation)
(print: $newLocation)
This sets the previous history for Bernie then sets Bernie’s location. Finally, the code prints out the location so you can track Bernie’s movements as you play the story. You’ll delete this statement in the next tutorial.
The final completed should look like the following:
{(set: $newLocation to -1)
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
(set: $availableRooms to _room's bernieExits)
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
]
]
]
(if: $newLocation is -1) [
(set: $newLocation to $availableRooms's first)
]
]
]
(set: $previousBernieLocation to $bernieLocation)
(set: $bernieLocation to $newLocation)
(print: $newLocation)
}
Seeing Bernie movement in action
You’ve added a lot of code. Now comes the time to call it. This is going to require a bit of copying pasting. To get started, open the Camp Entrance passage. Update the start of the passage to the following:
(set: $currentLocation to 1)(display: "Move Bernie")A worn looking sign says, "Welcome to Adams Pond Scout Camp" yet the complete silence makes you feel you have arrived at the end of the world.
By adding the (display :) macro, you’ve move Bernie from his starting location to a new location. Run the story. When you reach the Camp Entrance passage, you’ll see the ID of Bernie’s new location.

This places Bernie at the Dusty Road. Bernie is only one location away from the player. Move to another location, and then move back to the Camp Entrance. You’ll see that he moved again.

Bernie is now at the Hopi Campsite. He will follow this identical path each time you play the story because you’ve set the random seed in the Setup passage. That said, having a predetermined path is quite helpful for testing purposes.
Here is a map of Bernie’s early pathing:

Currently, Bernie only moves when you arrive at the Camp Entrance passage. Update the rest of the location passages to add (display: “Move Bernie”) passage to rest of the location passages. Make sure to put it after (set :) macro.

Now, Bernie will move wherever you go.
Warning the player
Now that Bernie is making his away across the campground, you should warn players when he is in an adjacent passage. Create a new passage. Call it Bernie Nearby. This is a rather simple bit of code. Add the following:
{
(set: $currentRoom to $rooms's ($currentLocation))
(for: each _exit, ...$currentRoom's exits) [
(if: _exit is $bernieLocation) [
(set: $isBernieNearby to true)
]
]
}
The code first acquires the current room from the array. The current location matches the ID of each room. For example, the Camp Entrance has an ID of 1. This corresponds the first element in the array. To review arrays, feel free to read this earlier article.
Once you have the room data, you loop through the exits and see if they match Bernie’s location. If they do match, then you know that Bernie is close at hand.
Add the following before the closing brace.
(if: $isBernieNearby is true) [
(print: "<br><br>You hear a soft whistle and footsteps in the dark.")
]
This will print out a message when Bernie is near. It won’t reveal the actual direction.
Once you set the bernieNearby variable to true, it will be true for the rest of the story. You need to set it back to false once Bernie has moved. Open the Move Bernie passage. Add the following before the end of the closing brace:
(set: $isBernieNearby to false)
Finally, open the Setup passage and add the following variable with your other variables:
(set: $isBernieNearby to false)
It’s a good habit to declare your variables in one place. Make sure to the put this variable with other variable declarations to keep things neat.

Finally, you need to call it. Open the Location Footer, and update it to the following:
{
(if: (passage:)'s tags contains "RoomItems") [
(display: "Bernie Nearby")
]
(if: (passage:)'s tags contains "Pickup") [
(display: "Pickup Location")
]
(if: (passage:)'s tags contains "RoomItems") [
(display: "Room Items")
]
(if: (passage:)'s tags contains "Inventory") [
(display: "Inventory")
]
}
You added a reference to the passage using the RoomItems tags. Now Bernie’s warning will appear in every passages tagged with the RoomItems. You may want to create a specific Bernie tag.
Play your story. You’ll hear Bernie right at the Camp Entrance.

You better get running!
Bernie’s movement problem
Unfortunately, Bernie has a small bug. When you start the story, he’s located at the Dusty Road, but if you pickup the flashlight, he’ll move again. The same occurs when you inspect the pickup. Every time the player interacts with the environment, Bernie moves. The same goes with the other passages.
This happens because every time a location passage is presented, Bernie is told to move. For example, when you start the game, you are presented with the Camp Entrance passage so Bernie moves. When you inspect the pickup, and return back to it, the Camp Entrance passage is presented again so Bernie moves.
This may cause player getting caught without them moving! For some, this may be a feature, but for your story, Bernie should only move when the player moves.
Open up the Setup passage. Add this variable to your growing collection:
(set: $playersPreviousLocation to 1)
This sets the previous location to the Camp Entrance. Now, open the MoveBernie passage. Wrap all your code the following (if :) macro like follows:
{(if: $currentLocation is not $playersPreviousLocation) [
(set: $playersPreviousLocation to $currentLocation)
(set: $newLocation to -1)
(for: each _room, ...$rooms) [
(if: _room's id is $bernieLocation) [
(set: $availableRooms to _room's bernieExits)
(if: $availableRooms's length > 1) [
(set: $places to (shuffled: ...$availableRooms))
(for: each _place, ...$places) [
(if: $newLocation is -1) [
(if: _place is not $previousBernieLocation) [
(set: $newLocation to _place)
]
]
]
]
(if: $newLocation is -1) [
(set: $newLocation to $availableRooms's first)
]
]
]
(set: $previousBernieLocation to $bernieLocation)
(set: $bernieLocation to $newLocation)
(set: $bernieNearby to false)
(print: $newLocation)
]}
It should look as follows:

Now, when you play the story, Bernie will move when you move. By setting the previousPlayerPosition to 1, Bernie will wait a turn before moving. This gives the player a little extra time at the start of the game.
Where to go from here
Congrats! You created a wandering NPC. Granted, the NPC doesn’t do anything or interact with the player at all. Don’t worry. That will come in the next tutorial. You can download the completed sample project here:
In the next tutorial, you’ll have the player interact with Bernie himself. You’ll define the win condition and the lose condition. It’s hard to believe you are getting close to finishing this story. The hard work is done. It’s all downhill from here so take a break and meet me in the next tutorial.
Discover more from Jezner Blog
Subscribe to get the latest posts sent to your email.