Verb handling in Cascadia Quest

In this post, I’ll go over some of how verb handling works in Cascadia Quest (as opposed to my older game Snail Trek).

The problem

Given that the bulk of the game consists of responses to actions on items, I need to make sure that the process of writing those handlers is as simple and non-bug prone as possible.

There is more to a verb handler than just the code that responds to it:

  • There also needs to be some metadata about the verb on that object – mainly, does it require the player to approach the object or not? ‘look’ might not, but ‘get’ usually will (but not always – it depends on the object).
  • We would also ideally like to know if the verb can be used “nounlessly” with the object. For instance, if there is a door and a button in front of the player, ‘open’ can be used nounlessly with the door, but not the button (but you could still type ‘open button’ and expect some kind of fallback response).
  • Some verbs that are normally treated as synonymous need to be treated separately on certain objects. ‘look’ vs ‘look in’ is one example. There are more that will be listed later.

The made-up language I’m using did not have a good way to declare verb metadata, so I was left with implementing other methods on an object that would return the necessary information about a verb (e.g. does it require approaching an object). It was much too easy for this to get out of sync with the actual verb handling code.

Far verbs vs near verbs (in this case, look vs get)

 

Additionally, the verb handling code was a basically a series of switch/if/cond statements. Writing logic like that, while straightforward, tends to lead to bugs. A more data-driven approach would be better.

Here’s an example of the old way, from the funny hat dispenser in Snail Trek 2:

(instance hatDispenser
	(method (remapToCoreVerb verb)
		; Normally vLookInside is mapped to vLook. I need to declare that it shouldn't be for this object
		(if (IsOneOf verb vLookInside)
			(return verb)
		)
		(return vNone)
	)

	(method (isApproachVerb verb)
		; Need to remember to put all approach verbs here
		(return (IsOneOf verb vUse vPut vLookInside))
	)
	
	(method (doVerb theVerb item)
		(switch theVerb
			(vKill
				(Print "If you break the machine, there will never be anymore funny hats.")
			)
			(vGet
				(Print "Don't get greedy.")
			)
			(vLook
				(Print "It's a small machine mounted in the wall that says \"Funny Hat Dispenser\".")
				(Print "It has a small coin slot.")
			)
			(vLookInside
				(Print "You peek inside the coin slot, but don't see anything.")
			)
			(else 
				; Logic getting a little complicated here
				(cond
					((or
							(and (IsOneOf theVerb vUse) (== item nNothing))
							(and (IsOneOf theVerb vPut vUse) (== item coin))
					 )
						(InsertCoin)
					)
					
					(else
						; Need to be sure to forward onto the default handlers
						(super doVerb: theVerb item &rest)
					)
				)
			)
		)
	)
)

The solution

Given that I have control over the language, it seemed to make sense to modify the language to support my needs – basically bubbling up the game logic requirements into the language syntax. The less friction between intent and implementation the better.

I didn’t do anything complex like change the compiled language structure to add metadata. Instead, I just made the compiler take the new syntax and convert it behind the scenes to a series of methods that works the old way.

Here’s the same object above, converted to the new way:

(instance hatDispenser
	; all these verbs will automatically be tagged as 'make player approach'
	(nearVerbs
		(vLookInside
			(Print "You peek inside the coin slot, but don't see anything.")
		)
		(vUse
			(InsertACoin)
		)
		(vPut, vUse -> coin
			(InsertACoin)
		)
	)

	; these work from a distance, but will be tagged for nounless support
	(farVerbs
		(vKill
			(Print "If you break the machine, there will never be anymore funny hats.")
		)
		(vGet
			(Print "Don't get greedy.")
		)
		(vLook
			(Print "It's a small machine mounted in the wall that says \"Funny Hat Dispenser\".")
			(Print "It has a small coin slot.")
		)
	)
)

In addition to being more terse, all the metadata for the verbs is inferred by inspecting the code. The compiler can generate the appropriate code behind the scenes that will inform the framework about all this at runtime.

A small side note: by default, any verb that is handled is considered as a candidate for a nounless verb (meaning you can omit the noun if you’re standing in front of the object). This can still cause issues in some cases: say you have two objects in front of you, and you can only actually ‘get’ one of them. But I wish to have a (perhaps funny) response for trying to ‘get’ the other one. I have not yet implemented any syntactical solution for this (I can still override the appropriate method on the object to solve this though).

Some more details

In each verb clause in a nearVerbs or farVerbs section, I can declare a number of verbs and items. For instance:

(vPut, vUse -> nMushroom, plant
;stuff
)

This will handle trying to put the mushroom or plant in this object, or using the mushroom or plant with this object.

The items supplied can be inventory items or other objects in the room. If no items are supplied, then it’s a handler for when the player just uses that verb with the object (nounlessly or noun), with no other objects mentioned:

(vUse
)

Note that in the old way, in this scenario it was very easy to forget to explicitly check that no item was supplied to the verb handler. So ‘open elevator with crowbar’ would be treated the same as ‘open elevator’. Sometimes that is desirable, but when that is the default behavior some issues can arise – especially with nounless matches. For instance, consider the case where there are a box and an elevator in front of the player. They type ‘open elevator’, but the box is closer to the player, and we do a nounless match with the box (which supports ‘open’). The result is that the box’s verb handler will be called with vOpen and elevator. Yes, I do nounless matching even when a noun is supplied. This is how you can walk up to Christian and type ‘ask about Shelley’ (turns into ‘ask Christian about Shelley’), or how you can stand in front of an engine and type ‘put fuel’ (turns into ‘put fuel in engine’).

In the case where we really do need to handle any item that is supplied, we can do this:

(vPut -> *
)

Then there is an implicit ‘item’ variable we can reference to identify which item was said (if we care).

Verb remapping

The verbs in Snail Trek and Cascadia Quest work on multiple levels.

First, there is a mapping from the typed words into conceptual verbs. This is handled by a grammar parser and isn’t always a 1-to-1 mapping. For instance, all the following typed phrases will be mapped to vEnter: ‘enter’, ‘get in’, ‘go down’. Or, vTurnOn can be ‘turn on’, but also ‘switch on’ or ‘start’. This minimizes the complexity of the code I need to write, and ensures consistency when performing actions on different objects. I’m constantly changing these as I develop the game and need to distinguish what were formerly the same verb. Currently in Cascadia Quest there are about 440 recognized grammar verbs, and these are mapped (often along with prepositions) to nearly 100 different conceptual verbs that I write code against (stick that in your point-and-click UI!).

Many different intentions for jump. In some cases we want to differentiate (the toilet), in others (the fire) we treat them all the same.

 

It’s often useful to distinguish conceptual verbs in some cases but not others. For instance, looking at a bed might result in a different response than looking under the bed. So there are multiple look verbs: vLook, vLookUnder, vLookInside, vLookBehind, vSearch. By default, these will be mapped to the first one, vLook. So a cupboard, say, can just have a vLook handler, and ‘look in cupboard’ will just result in the cupboard’s vLook response. But if a nearVerbs or farVerbs explicitly handles one of the others, then we will automatically make a distinction.

In the old way, I had to write additional code to tell the framework not to automatically remap these conceptual verbs to the more core set of conceptual verbs. You can see that in the example posted earlier in this post:

(instance hatDispenser
	(method (remapToCoreVerb verb)
		(if (IsOneOf verb vLookInside)
			(return verb)
		)
		(return vNone)
	)
)

Other examples where this remapping currently happens are: vKill (vCut, vHit). Or vJump (vJumpOff, vJumpOn, vJumpOver).

The next post will go into more detail on some of the subtler verb handling issues.

2 Comments on “Verb handling in Cascadia Quest

  1. And then I adapted the basics of the (verbs) block for use in SCI11. There’s no text parser and items being used are basically the same as regular actions (look, use, talk to, use crowbar, use gravgun), so it’s quite simplified compared to all this. You still get a lot of readability gains though.

  2. Pingback: More verb handling tidbits – Cascade Quest blog

Leave a Reply

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