-- everything is a location, an object or a timer --
Before we start working on our story we must define some basic stuff
that we can reuse with other stories. Things like:
common descriptions;
common flags;
common attributes;
common triggers;
logging;
save and restore functions (to store progress);
regression testing;
the player object;
mechanism for starting the game;
mechanism for moving the player between locations;
looking around;
scoring mechanism;
verbose function;
quit function.
At this point it is helpful to read sections “locations objects and
timers” (2 pages) and “location and object artifacts” (5 pages) from
the XVAN Introduction document.
Inputs for this part of the tutorial are files
part1-end.lib and
part2-start.xvn.
Common descriptions are descriptions that each location and object has.
This is the reason they can be used with wildcards: it is guaranteed
that they can be found at a later time when the wildcard is linked to an
actual object or location.
We will use the following common descriptions (apart from the predefined
descriptions):
d_longdescr long location or object description;
d_shortdescr short object description;
We make the following design decisions:
For locations, d_shortdescr always is the location name only.
Upon first visit of a location, the location's d_shortdescr and
d_longdescr are printed;
With following visits only d_shortdescr is printed;
For an object, with each visit only d_shortdescr is printed;
For objects the “examine” command prints d_longdescr;
For locations the “examine” command acts as the “look” command (see
further);
We will also define a verbose() function to allow forced printing of
long location descriptions at all times.
We will use the following common flags (apart from the predefined
flags):
f_seenbefore to determine whether to print long or short
description.
We will use the following common attributes (apart from the predefined
attribute):
r_be conjugation of verb ”to be” for location or object;
r_have conjugation of verb “to have” for location or object.
We must also define words for is, are, has and have in the vocabulary
file. I chose to define them as verbs so they can also be used in user
input (e.g. "where is the toaster"). If you only want to use them as
attribute values for printing, they may have any word type.
Common triggers
We will use the following common triggers (apart
from the predefined triggers):
t_i to print inventory
t_exa to examine
We make the following design decisions:
When the player object enters a new location the predefined trigger
t_entrance will be executed for the new location and all its
contained objects;
When the player object wants to exit from a location the predefined
trigger t_exit will be executed for the current location and all its
contained objects. If any of them responds with disagree() the
player will not be allowed to leave the location;
We will now define the common triggers.
t_i
This trigger was already mentioned in part 1 of the tutorial. The
inventory verb will print the "You are carrying" message and each object
will print its description.
Our t_i common trigger
t_i
if owns(o_player, %this) then
indent() # indent level
was set by the verb prologue
printcr("[a] [this]")
else
nomatch()
Because this is a common trigger, each object and location will now have
this trigger (you don’t have to worry about memory space, the code is
only stored once). In case an object needs to print a different message
(e.g. add text like "being worn") it suffices to redefine the trigger as
a local trigger with the same name within the object body. Local
triggers take preference over common triggers with the same name.
There's something new, here. The function nomatch() generates the third
possible return code for a trigger (the others are agree() and
disagree() as explained in part 1). With nomatch() a location or object
tells the interpreter "forget that I had a match for this input". But
why do we need nomatch(), we can also print nothing and return agree(),
right? Wrong. If we return agree(), the interpreter would know there had
been a match and no verb default code would be executed. So if the
player carried nothing and all objects in the location would return
agree(), the default verb text "nothing, you are empty-handed" would not
be printed.
Nomatch() is quite powerful. If you redefine a common trigger and the
local copy returns nomatch(). the interpreter will execute the common
trigger as well. I use this sometimes to let the object do a quick test
and if everything is ok, execute the common trigger after all.
Our design decision states that we should print d_longdescr.
Our t_exa common trigger
t_exa
printcr(d_longdescr)
setflag(f_seenbefore)
the interpreter will know for which object or location the common
trigger is executed, so it can locate the right description and flag. We
could also have said printcr(%this.d_longdescr) and
setflag(%this.f_seenbefore).
As per our design decision, t_entrance is triggered when the player
object enters a new location. We want it to:
print information about the location (long or short description);
print information about the objects in the location;
print information about objects in/on/under/.. other objects.
Here we go, comments added for clarification.
t_entrance
if not(islit(o_player)) then
printcr("It is pitch black.")
disagree() # ready, exit
endif
if equal(%this, l_location) then
# l_location is wildcard for the current location
printcr(d_shortdescr) # print location name
if not(testflag(f_seenbefore)) then
# first visit
setflag(f_seenbefore)
printcr(d_longdescr)
endif
else
# it´s not the current location but an object in the location
if cansee(o_player, %this) then
if owns(owner(o_player), %this) then
# object is at the same containment level as player
setflag(f_seenbefore)
printcr(d_shortdescr)
else
if not(owns(o_player, %this, 0)) then
# it´s not (in) some object the player carries (0 means all
levels of containment)
setflag(f_seenbefore)
print("There is [a] [this] [r_preposition] [the] ")
print(owner(%this))
printcr(".")
endif
endif
endif
endif
# all the endifs are not necessary at the end of a trigger.
So, what t_entrance does:
check whether it's dark;
check whether it's executed for the location (as opposed to an
object in the location);
check whether it's executed for an object in the location that the
player can see;
check whether this object is contained in another object not carried
by the player if the object is carried by the player, we don’t want
to mention it).
Note: make sure to close the current IF-statement with an ENDIF when
starting a new if statement. If you find that parts of your trigger
should be executed but are not, you may have forgotten an ENDIF
statement. If you forget the first ENDIF (line 5), nothing of the
trigger will be executed when the player is lit because all lines will
be considered part of the if not(islit(o_player)) branch.
In case the common t_entrance should not be executed for an object,
define an empty t_entrance trigger locally with the object. Empty as in
that it only contains an agree() function. We do this for example for
the player object.
As per our design decision, t_exit is triggered when the player object
exits the current location. We want it to:
check whether the player object is free to go.
if not, it must return disagree()
As leaving a location is game specific, in our basic definitions the
t_exit trigger will always return agree().
t_exit
agree()
But how does it work then? We'll come to that when we define the player
object, but here's a heads-up: XVAN has a function called exit(par).
This function will call t_exit for par and all its contained objects. If
one of the t_exit triggers returns disagree(), the exit function will
return false and we know there's some object not allowing the player to
exit the current location. Likewise, there is also an entrance(par)
function that calls all t_entrance triggers.
To log your game session, XVAN has the built-in transcript() function.
This function copies user input and the game's response to a file called
transcript.txt in the directory that the game is running from. Calling
transcript for the second time will turn off logging. We will define
transcriptas a verb in the vocabulary file so it can be used for all
games.
VERB transcript
"transcript"
transcript()
DEFAULT
printcr(“use ‘transcript’ to log your session.”)
ENDVERB
To store and load game progress, XVAN has functions save() and
restore(). The save() function stores the current story progress in a
file called save.dat in the directory that the game is running from. The
restore() functions scans the directory for save.dat and loads it.
In order to use the functions, we create verbs “save” and “restore” in
the vocabulary file. Restoring a game is pretty straightforward and
always allowed, so we will define the restore functionality in the
vocabulary file. The code for saving will be a local trigger in the
player object. Why? Well, there may be game specific situations when we
do not allow the user to save. For example in a maze or to prevent
trial-and-error guessing when solving a puzzle.
$VERB save
# define your save functionality in the story file
DEFAULT
printcr(“Use ‘save’ to save your progress.”)
ENDVERB
$VERB restore
"restore"
restore()
printcr("restored.")
DEFAULT
printcr(“use ‘restore’ to restore a previously saved game.”)
ENDVERB
$VERB quit SYNONYM q
"quit"
print("Do you really want to quit? ")
if yesno() then
quit()
DEFAULT
printcr("use 'quit' to leave the game.")
ENDVERB
The yesno() function requires the user to enter "yes", "no", "y" or "n".
It is not case sensitive.
The player object is mandatory in each XVAN story file.
Before we start, the player object in
part2-start.xvn looks like this:
$OBJECT o_player
# The o_player object is predefined and represents the human player.
DESCRIPTIONS
d_sys
CONTAINED
FLAGS
ATTRIBUTES
TRIGGERS
END_OBJ
For starters, we will:
set the system descriptions (d_sys) to “you” and "me";
set common attribute r_be to “are”;
set common attribute r_have to "have";
define the nouns "you"and "me" and the "are" verb in the vocabulary
file;
override common triggers t_entrance and t_exit with local ones that
do nothing.
$OBJECT o_player
# The o_player object is predefined and represents the human player.
DESCRIPTIONS
d_sys “You”, "me"
CONTAINED # don’t know where the player starts until we have
the game map
FLAGS
ATTRIBUTES
r_be = are # you are
r_have = have # you have
TRIGGERS
t_entrance
agree()
t_exit
agree()
END_OBJ
Next, we will define the following basic stuff in the player object:
starting the game;
moving the player between locations;
looking around;
keeping the score;
save and restore commands to store progress;
logging;
verbose function.
If we do nothing, the game will just start with a “> “ prompt. But we
want to print some introductory text when the game starts. XVAN has no
default starting mechanism, so we make our own. We define a timer m_init
that reaches its threshold when the game starts.
m_init
init 0
step 1
direction up
interval 1
state go
trigger_at 1
execute o_player.t_init
With the player object, we will define a trigger t_init and a
description d_init. The trigger prints the description.
$OBJECT o_player
# The o_player object is predefined and represents the human player.
DESCRIPTIONS
d_sys “You”, "me"
d_init “*** XVAN tutorial ***”
CONTAINED # don’t know where the player starts until we have
the game map
FLAGS
ATTRIBUTES
r_be = are # you are
r_have = have # you have
TRIGGERS
t_entrance
agree()
t_exit
agree()
t_init
printcr(d_init)
printcr("")
entrance(owner(o_player))
END_OBJ
After starting the game, timer m_init expires and triggers
o_player.t_init. This will print d_init, our opening message and
describe the player's initial location.
XVAN has no default mechanism to let the player move between locations.
We will create our own mechanism. We will use several of XVAN’s built-in
functions to implement moving around. XVAN’s built in functions are
described in detail in a separate document.
In order to move around the player we must:
make the player object catch user input about moving around;
check if the direction indicated in the user input is a valid
direction;
check with all objects in scope whether the player may leave;
move the player object to the new location and execute the
t_entrance triggers.
This is our new player object
$OBJECT o_player
# The o_player object is predefined and represents the human player.
DESCRIPTIONS
d_sys “You”, "me"
d_init “*** XVAN tutorial ***”
CONTAINED # don’t know where the player starts until we have
the game map
FLAGS
ATTRIBUTES
r_be = are # you are
r_have = have # you have
TRIGGERS
"[dir]" -> t_move
“go to [dir]” -> t_move
t_entrance
agree()
t_exit
agree()
t_init
printcr(d_init)
printcr("")
entrance(owner(o_player))
t_move
if valdir(l_location, %dir) then
# it's a valid direction
if exit(l_location) then
# no object objects to the player leaving the room
move(o_player, %dir) # move updates current location
entrance(l_location)
endif
else
nomatch() # let other objects or verb default code react.
endif
agree()
END_OBJ
What do we see here? Right below the TRIGGERS keyword we see two
possible user inputs that will fire the t_move trigger. In the t_move
trigger, the valdir() function checks if the direction is a valid
direction. If not, the t_move trigger returns nomatch(). In case none of
the other objects react, the default verb code will be executed which
will print “you can’t go that way”.
If the direction is valid, the exit() function will execute the t_exit
triggers from all objects in the current location (and from the current
location itself). If all return agree(), then the player object will be
moved in the direction indicated by the user input. Finally, for the
new location and all its contained objects, the t_entrance trigger will
be executed.
You may have noted that move() accepts different kinds of parameters.
With the verbs take and drop, we used move(object1, object2) which moved
object2 in object1. With t_move we used move(object, direction) which
moved object to the location that is reached by going into the
direction. Possible parameters combinations are listed in the function
description document.
To enable the player to look around we define trigger t_look locally
with the player object.
This trigger will call the entrance() function for the player’s
location. This means that for the player’s location and each containing
object, the t_entrance trigger will be called.
The player’s t_look trigger
t_look
if equal(owner(o_player, l_location)) then
clearflag(l_location.f_seenbefore)
entrance(l_location)
else
# the player is in some object. Print this information
print(“[[[prepos] [the] “)
print(owner(o_player)
print(“].”)
clearflag(owner(o_player).f_seenbefore)
entrance(owner(o_player))
Because we (ab)use the entrance() function in our look command, we must
clear the f_seenbefore flag before calling the entrance() function. The
reason is that the t_entrance trigger will check for f_seenbefore and if
it's set it will print the short description.
Why are there 3 ‘[‘ in the print statement? We want to print something
like “[in the boat]”. However, for the compiler, a ‘[‘ in a string means
that a parameter will follow. We tell the compiler to print one ‘[‘ by
entering ‘[[‘. So, ‘[[[‘ tells the compiler to print a ‘[‘ and that a
parameter will follow.
Note that just defining t_look does not mean that the t_look trigger
will be executed when the user enters “look”. We must yet link the
trigger to a user input. This is done at the beginning of the TRIGGERS
section in the player object:
“look” -> t_look
Now the interpreter knows that whenever the user enters “look”, it must
execute the t_look trigger.
To keep track of the score, we define a local attribute r_score with the
player object. We also define a verb “score” in the vocabulary and a
local trigger t_score for the player object that prints the score.
We create a local trigger t_save with the player object. Additionally,
we create a local flag f_no_save with the player object. The t_save
trigger will check the flag and if it is set, it will not save game
progress. This can be used to prevent cheating. For example, when the
player enters a maze, an object may set the o_player.f_no_save flag and
thus prevent the player from saving progress while he is in de maze.
By invoking the verbose functionality, the t_entrance triggers will
always print the long location descriptions (d_longdescr). We define the
verb ‘verbose’ in the vocabulary file and a local flag f_verbose with
the player object. Next we change the common t_entrance trigger so it
will test for the f_verbose flag before printing the location
description.
This is what we have right now:
In the vocabulary file we’ve added:
$VERB score
DEFAULT
printcr(“Use ‘score’ to get information about your score.”)
ENDVERB
VERB verbose
DEFAULT
printcr(“use ‘verbose’ to toggle long room descriptions.”)
ENDVERB
‘score’ and ‘save’ will be handled by the player object. All other
input with these verbs will print the default message.
Our player object with all the basic stuff we wanted:
$OBJECT o_player
# The o_player object is predefined and represents the human player.
DESCRIPTIONS
d_sys “You”, "me"
d_init “*** XVAN tutorial ***”
CONTAINED # don’t know where the player starts until we have
the game map
FLAGS
f_no_save = 0
f_verbose = 0
ATTRIBUTES
r_be = are # you are
r_have = have # you have
TRIGGERS
"[dir]" -> t_move
“go to [dir]” -> t_move
“look” -> t_look
“score” -> t_score
“save” -> t_save
“verbose” -> t_verbose
t_entrance
agree()
t_exit
agree()
t_init
printcr(d_init)
printcr("")
entrance(owner(o_player))
t_look
if equal(owner(o_player), l_location) then
clearflag(l_location.f_seenbefore)
entrance(l_location)
else
# the player is in some object. Print this information
print("[[[prepos] [the] ")
print(owner(o_player))
printcr("].")
endif
if cansee(o_player, owner(owner(o_player))) then
entrance(owner(owner(o_player)))
else
entrance(owner(o_player))
t_move
if valdir(l_location, %dir) then
# it's a valid direction
if exit(l_location) then
# no object objects to the player leaving the room
move(o_player, %dir) # move updates current location
entrance(l_location)
endif
else
nomatch() # let other objects or verb default code react.
endif
agree()
t_score
printcr(“Your score is [r_score] points.”)
t_save
if testflag(f_no_save) then
printcr(“Saving at this point would be like cheating.”)
else
save()
printcr(“saved.”)
t_verbose
if testflag(f_verbose) then
clearflag(f_verbose)
printcr(“Verbose mode turned off.”)
else
setflag(f_verbose)
printcr(“Verbose mode turned on.”)
END_OBJ
And our common t_entrance trigger, adapted for verbose functionality:
t_entrance
if not(islit(o_player)) then
printcr("It is pitch black.")
disagree() # ready, exit
endif
if equal(%this, l_location) then
# l_location is wildcard for the current location
printcr(d_shortdescr)
if not(testflag(f_seenbefore))
or testflag(o_player.f_verbose) then
# first visit
or verbose mode
setflag(f_seenbefore)
printcr(d_longdescr)
endif
else
# it´s not the current location but an object in the location
if cansee(o_player, %this) then
if owns(owner(o_player), %this) then
# object is at the same containment level as player
setflag(f_seenbefore)
printcr(d_shortdescr)
else
if not(owns(o_player, %this, 0)) then
# it´s not (in) some object the player carries (0 means all
levels of containment)
setflag(f_seenbefore)
print("There is [a] [this] [r_preposition] [the] ")
print(owner(%this))
printcr(".")
endif
endif
endif
endif
# all the endifs are not necessary at the end of a trigger.
This ends part 2 of the tutorial. We’ve finished our preliminary work
that we can use as a starting point for future stories.
Everything we did is in files part2-end.lib
and part2-end.xvn. These files are the
starting point for part 3 of the tutorial where we will write our sample
story.
The files we created in part 2 will not compile to an XVAN story. This
is because part2-end.xvn is not yet complete. By the end of the next
part 3 we will have a playable story.
.