Exits revisited

In the past weeks, I’ve been adding more and more content to the game and I noticed something that I wanted to fix. When I had a room with a number of exits, they would all be listed one after the other.

Exits are leading North, West and South.

While there’s nothing technically wrong with this approach, it just looks very utilitarian and kind of breaks the atmosphere of an actual narrative. The more I work on the game, the more I realize that I want the computer to act more like a narrator than just a tool that provides means to an end.

To avoid this issue, I’ve decided to create a more flexible way to list exits, using random sentence templates to mix things up, but also to break the long list in a series of individual sentences. Instead of having one sentence listing four exits, for example, I’d rather have two sentences, listing two exits, each. This way, my output could look like this.

From here you can go North to the Library or West. The Common Room lies to the South.

It has much more of a literary feel, wouldn’t you agree?

Interestingly, this turned out to be quite a bit more complex than I had anticipated… but at least it’ll make for an interesting blog post, n’est-ce pas?

I tried to solve the problem with a brute force approach at first, sitting down coding something… but that didn’t get me very far. Things got too confusing too quickly. So I decided to work in stages.

First I needed to create a few example sentences so that I could work out what parts need to be dynamically replaced during the generation. To wrap my head around them, I wrote them like this at first, creating quasi-templates.

From here you can go A or B
The only exits here are A and B
Exits are leading A and B
There is [A DOOR/ENTRANCE/PORTAL/…] leading A and B lies to the [NORTH/WEST/…]

The important thing to note here is that A, for example, is also a template in itself that can expand into different versions, like this…

From here you can go [WEST].
From here you can go [WEST to the HALL/BEDROOM/…]

This kind of pseudo-language made it easier for me to create placeholders. It also told me, what kind of information I would need to properly fill the different templates for any of the possible exits.

As you can see from these templates I will need three pieces of information

  • the direction
  • the name of the next room, if it is known
  • the type of the exit

From these pseudo-sentences, I then derived the following stub templates with the {0}, {1} and {2} placeholders representing these three data types.

_s1 = "{0}"
_s2 = "{0} to the {1}"
_s3 = "a {2} leading {0}"
_s4 = "a {2} leading {0} to the {1}"
_s5 = "the {1} lies to the {0}"

Next, I created string templates that would use these stubs to create the actual sentence. This makes it possible to randomize and dynamically create these stubs as well as the actual output sentence. I like that because it adds variation to the game.

_l1 = "The only exit here is {0}"
_l2 = "The only exits here are {0}"
_l3 = "From here you can go {0}"
_l4 = "Exits are {0}"
_l5 = "There is {0}"

When you take a look at this, you will see that some of these templates can be combined, whereas others cannot. _l3 and _s1 work fine, _l3 and _s2 work fine, but _l3 and _s5 do not work. Therefore, I needed to build some kind of logic that combined the correct templates and sub-templates as the final sentence was built. And that is where things got tricky.

I needed a plan so I wrote out the program flow in pseudo code just so I could stay on top of the logic.

# if no exits
#   print No Exits
# else create exit response
#    loop
#       if not visited
#         use stub S1
#         if DescName not empty
#           use stub S3
#       if visited / else
#         use stub S2
#         if odd counter
#           use stub S5
#         if DescName not empty
#           use stub S4
#       if even count
#       possible templates L2 and L4
#         if one exit
#     use template L1
#         if DescName is not empty
#           if stub is S3 or S4
#            possible templates also L5
#           else
#            possible templates also L3
#       format output
#      else
#        extend template
#        format output
#        add output to OutString

As you can see, there is a bit of logic involved in picking the right responses and piecing them together in a way that makes sense.

The next step was, of course, to implement this module. We have our string templates already, so let’s dive right into the loop to generate the new list of exits.

	_counter = 0
	_outString = ""
	if 0 == len ( self.Exits ):
		_outString = "Strangely, you can see no exits here"

		_descriptive = False
		_firstResp = True
		_numLeft = len ( self.Exits )
		for _xit in self.Exits:								# grab the current exit
			_adjacent = rooms.theRooms [ _xit.Target ]		# get the adjacent room

			if not _adjacent.HasBeenVisited:				# room has not been visited
				_stub = _s1
				if _descriptive or _xit.DescName:
					_stub = _s3
			else:											# room has been visited
				_stub = _s2
				if _counter & 1:
					_stub = _s5

				if _descriptive or _xit.DescName:
					_stub = _s4

			if not _counter & 1:
				_templates = [ _l2, _l4 ]

				if 1 == len ( self.Exits ):					# if there's only a single exit in the room
					_templates = [ _l1 ]

				elif 1 == _numLeft:							# if there's one exit left in the enumeration
					if not _xit.DescName:
						_templates = [ _l7 ]
						_templates = [ _l6 ]

Up to this point, I have started looping through the exits in the room and during each iteration, I am checking how much information I have about the current exit. With that in mind, I pre-fill an array with suitable templates for this particular exit and prepare a stub with the exit info that can be used to fill the template—which what I am doing here…

				if _xit.DescName:
					if _s3 == _stub or _s4 == _stub:
						_templates.append ( _l5 )
						_descriptive = True
						_templates.append ( _l3 )
				_template = random.choice ( _templates )
				_insert = _stub.format( _xit.Describe (), _adjacent.Name, _xit.GetName ( globals.Articles.Undef ) )
				_outString += _template.format ( _insert )
				_numLeft -= 1

				_template = " and {0}. "
				_insert = _stub.format( _xit.Describe (), _adjacent.Name, _xit.GetName ( globals.Articles.Undef ) )
				_outString += _template.format ( _insert )
				_numLeft -= 1
				_descriptive = False
				_firstResp = False

Depending on whether it is known where the exit leads to (meaning, has that room been visited before?) I am picking a random template from my array and fill in the missing information using the stub I prepared earlier. The string.format() function is really useful for this, as it turns out because I can feed parameters into it regardless of whether the template actually uses them or not.

I am also setting some flags so I know whether this is the first exit I am describing in the current sentence or the second one. Remember, the idea is to only list two exits per sentence.

All that is left to do now is to increment the counter and continue the main loop. I iterate through and handle all exits, then I terminate the sentence with a period if need be and print the resulting string to the screen.

			_counter += 1

	if _counter & 1:
		_outString += ". "

	print ( _outString )

It all seems pretty straightforward now that I look back, but as I mentioned, putting it all together and getting the logic right took a little bit of time. I hope this little piece of code will help you save that time if you ever encounter the same challenge.

Leave a Reply

Your email address will not be published. Required fields are marked *