Binding commands

Text adventure games respond to commands entered by the player. Some typical commands might be:

  • north
  • take wand
  • give wand to wizard

adventurelib lets the programmer write code that will run when a command is entered. This means you can decide what happens in response to a command. Your code might decide whether the wizard wants the wand and print what the wizard says when he gets it.

Note that you also needs to check that the wizard is here and you have the wand to give to him!

@when decorator

The @when decorator is written on the line above a function. The function will then be called when a player types a matching command.

This code will be called when the player types “scream”:

@when("scream")
def scream():
    print("You unleash a piercing shriek that reverberates around you.")

Note that this is case-insensitive, so it will also be called when the player types “SCREAM” or “sCrEAM”:

> scream
You unleash a piercing shriek that reverberates around you.

> SCREAM
You unleash a piercing shriek that reverberates around you.

You can put multiple words into the command. You can also write more than one @when line, which means the function will be called if any of the commands match. This can make it easier for the player to work out what to type:

@when("shout loudly")
@when("shout")
@when("yell")
def yell():
    print("You bellow at the top of your lungs.")

And then in game:

> yell
You bellow at the top of your lungs.

> shout loudly
You bellow at the top of your lungs.

As with the case of what a player types, so too the spacing of what a player types doesn’t matter:

>   shout        loudly
You bellow at the top of your lungs.

All the words you want the player to type have to be in lower case letters.

Capturing values

While you could write separate functions for “take wand” and “take hat”, it’s more normal to write a single function that will be called when the player types “take anything”.

This code will be called when the player types “take anything”, and the words that match the anything will be passed into the function so that you can react to what it was they tried to take:

@when("take THING")
def take(thing):
    print(f"You take the {thing}.")

So, in a game:

> take hat
You take the hat.

> take horse
You take the horse.

> take cheeseburger
You take the cheeseburger.

Of course, this isn’t a very useful function, because it does not check that there is a thing to take! You will have to write the code that does these checks.

Here’s another example, where we capture two words:

@when("give ITEM to RECIPIENT")
def give(item, recipient):
    print(f"You give the {item} to the {recipient}.")

Here are the rules for what you can write:

  • All the words you want the player to type have to be in lower case letters.
  • Words that you write in CAPITAL LETTERS will match any word the player types.
  • For each word you write in CAPITAL LETTERS, the function has to take a parameter with the same name in lowercase letters.
  • The function will be called with the names the player typed - but they will be converted to lower case.

Capturing multi-word names

An UPPERCASE name can match multiple words. If your code contains the above example:

@when("give ITEM to RECIPIENT")

Then a player can type:

> give poison apple to evil godmother

And your code will receive the values:

item = "poison apple"
recipient = "evil godmother"

As long as you require players to type some command words between ITEM and RECIPIENT (to in this case), this will do what you expect. But beware of providing a shorter alias:

@when("give ITEM RECIPIENT")

Adventurelib uses what’s called a greedy algorithm - “greedy”, because the first group will hungrily “eat” as many words as it can. If a player typed:

> give poison apple evil godmother

Then ITEM will “eat” the first three words, and your code will receive the values:

item = "poison apple evil"
recipient = "godmother"

Which is probably not what you expect!

However, each CAPITALISED word will match at least one word. So give apple godmother will do what you expect. Therefore one solution is to make sure every object in the game can be referred to by a single-word name like apple. This can work well in simple games, but the drawback is that you would struggle to create puzzles that involve multiple variations on an object:

> inventory
You have:
a red apple
a blue apple

> feed red apple to water nymph
The nymph sticks out her tongue and shivers unenthusiastically.

> feed blue apple to water nymph
The nymph's eyes widen as you take out the blue apple. She dashes
towards you and snatches it from your hands, and then immediately
turns and runs towards the small door.

Glancing back towards you momentarily, she wordlessly tosses you
a slender, silver-blue key, and a moment later is gone.

It is probably best to require words like to, with and on, so that adventurelib knows how to split up a phrase:

@when('give ITEM to RECIPIENT')

@when('use ITEM on TARGET')

@when('hit TARGET with WEAPON')

Additional parameters to commands

In some cases, you might like to use a function to handle a number of similar commands.

You can pass additional keyword arguments to the @when decorator which will be passed into the handler function whenever that version of the command line matched.

For example:

@when('shout', action='bellow')
@when('yell', action='holler')
@when('scream', action='shriek')
def shout(action):
    print(f'You {action} loudly.')

Calling @when functions yourself

Even though you’ve written a @when function and it will be called automatically when the player enters that command, you can still call the function yourself normally.

For example, if you write a look command, you can call this from other commands, such as when you enter a room:

@when('look')
def look():
    print(current_room)


@when('go north'):
def go_north():
    global current_room
    current_room = current_room.north
    print('You go north.')
    look()

Command Contexts

In some games, a command might only be available in certain contexts, or might change its behaviour in some contexts.

The most simple way of checking if a command can be used right now is to add an if statement:

@when('exit')
def exit_room():
    global current_room
    if current_room.outside:
        current_room = current_room.outside
    else:
        say("Exit what? You're already outside.")

This isn’t always the best way. In some cases there are just too many different conditions to check, and you would end up writing too many if/else statements. This can be useful in situations like these:

  • If you have levels then certain actions might only be available in one of the levels.
  • If you have a menu - a main menu, or an inventory menu perhaps - then you might have a different set of commands in that menu.
  • If you can “unlock” certain commands as you progress through the game.

The command context system allows you to configure some of your commands to be available in certain contexts only.

To do this, pass a context= keyword argument to the @when decorator:

@when('cast SPELL', context='wonderland')
def cast(spell):
    say(f"You cast the spell.")

Now this command will be completely hidden in help and in the game:

> cast fireball
I don't understand 'cast fireball'.

This command will only become active when we set the context to match. You can set and get the context using set_context() and get_context():

adventurelib.set_context(new_context)

Set the current command context to new_context.

Pass None to clear the current context.

adventurelib.get_context()

Get the current command context.

So for example:

@when('enter mirror')
def enter_mirror():
    if get_context() == 'wonderland':
        say('There is no mirror here.')
    else:
        set_context('wonderland')
        say('You step into the silvery surface, which feels wet and cool.')
        say('You realise that clicking your heels will let you return.')


@when('click heels', context='wonderland')
def click_heels(spell):
    set_context(None)
    say('The moment your heels touch the world rearranges around you.')

Now you can transition between the different contexts:o

> enter mirror
You step into the silvery surface, which feels wet and cool.
You realise that clicking your heels will let you return.

> help
enter mirror
cast SPELL
click heels

> enter mirror
There is no mirror here.

> cast fireball
You cast the spell.

> click heels
The moment your heels touch the world rearranges around you.

> cast fireball
I don't understand 'cast fireball'.

> click heels
I don't understand 'click heels'.

Note that any commands specified without passing context= will be available in all contexts.

You might want to call set_context() before you call start() in order to set the context that the game will start in.

Tip

Note that if you are not in the right context, the command will not appear at all. Beware of confusing your users with appearing and disappearing commands.

Context Hierarchies

Contexts may be nested inside other contexts. To do this, use a . character to separate different levels of the context hierarchy:

@when('land', context='wonderland.flying')
def land():
    set_context('wonderland')
    say('You gradually drop until you feel the earth beneath your feet.')

When the current context is wonderland.flying, all the wonderland commands are available as well as wonderland.flying commands and all commands specified without context=.

When the current context is wonderland, the land command will not be available:

You dance through the sky like a feather on the wind.

> land
You gradually drop until you feel the earth beneath your feet.

> land
I don't understand 'land'.

The most deeply nested context takes priority. You can use this to pass different parameters to a command in different contexts, or call a different function entirely:

@when('north', dir='north')
@when('north', dir='south', context='confused')
def go(dir):
    ...

@when('north', context='confused.really')
def confused_north():
    say('The cauliflowers are in bloom this year.')