Animate Arts 2 Spring 2005
Programming exercise 2

Out: Tuesday, May 10
Due: Rough cut: Friday, May 20, 11:59 pm
  Rough cut crits: Tuesday, May 24, 11:59 pm
  Final version: Thursday, June 2, 11:59 pm

Overview

In this assignment, you will make a text and/or point-and-click adventure game using simple object-oriented simulation techniques in Meta.

Getting the software

This assignment requires a new version of Meta.  So before starting the assignment, you should uninstall your current version and get the latest version from the web site.  Next, you should download the code for the assignment and unzip the file into a directory in some convenient place.

Running the sample game

Double-click the file Adventure.meta (or otherwise start Meta and open the file), and choose Execute All from the menu.  Then open the file Sample game.meta, do Execute All, and run the procedure: [sample-game].

Here are the text commands it will recognize:

For commands that take a Thing as an argument, the thing must be in the room the player is presently in.  The object can be specified by its name or by its type.  That is, if there's a door called "symposium", you can say either "go to the door" or "go to symposium".

If you want to write a game where players can specify objects by their adjectives (as in "go to the big door" versus "go to the little door"), let me know and I'll hair up the parser and send you the revised version.

Try out the game, and then look through the code in Sample game.meta.  Make sure you have a rough understanding of how it works.

Creating your own game

Now make your own game.  Start by making a new .meta file in the directory and adding its name to the variable source-files in the file Adventure.meta (place it at the end of the list of filenames so that it loads after the other files).  Now make a procedure, run, as follows:

[define run
           [→ place code here
                [the-player ← [new Player "some name" some-place]]
                other code here (optional)
                [start-game "game name" width  image-height  text-pane-height  input-height]]]

Fill in the italics as appropriate:

Place code here
This is where you put the code to make the rooms (Places) of your game (see making a new place, below).  You can leave it blank, initially.
Some name
This is the name for your player's character.  It's not really used anyplace in the code, so it doesn't really matter what name you choose.
Some-place
Fill this in with the name of the place you want to have the player initially appear in.  You won't be able to fill this part in until you make a room.
Other code here
This is optional; in case you want to put in some code that uses the-player (in which case you wouldn't want to run that code before the-player was initialized).
Game name
This is a text string that will be displayed in the title bar of the game window.
Width
This is the width (in pixels) you'd like for the game window.  In this sample game, this is set to 800 pixels.
Image-height
This is the height (in pixels) of the area you want to display the image in.  If you set it to null, the system won't display images.  In the sample game, this is set to 600 pixels.
Text-pane-height
This is the height of the window for text output.  Again, if you set it to null, it won't output text.  In the sample game, this is set to 100 pixels.
Input-height
The is the height for the text input area.  Again, set it to null if you don't want the user to be able to type commands.  In the sample game, this is set to 100 pixels.

What to do

Your job is to build a game that you like.  In order to prevent you from deciding that the game you like is outrageously simple, we are placing a few restrictions on your game:

If you add new fields or methods to existing classes, you can count those toward the total fields and methods.  For example, some of you will undoubtedly want to add a combat system, in which case you need to add things like hit points and magic points to the basic Player class.  Or you may want to add an inventory system (meaning give the player the ability to carry stuff) in which case you need to give the player pockets.

We've written up a FAQ for how to do simple things.  Write to Ian with any questions you have for how to do fancier things (i.e. "how do I make places display different pictures at different times?") and I'll add them to the FAQ.

Turning it in

There are three due dates for this assignment.  First, you will turn in a partial version of your game in a week and a half.  You must have at least 1/4 of the total work done, or 2 places, 4 objects, 2 new classes, 1 new field, and 2 new methods, to get full credit for this part.  However, there will be no extensions on this part because it will be critiqued.  So if you don't have it done yet, upload whatever you can or you will miss the crit entirely and get a zero for it.

To turn in your assignment (both for part 1 and part 3), make a new zip file containing the complete Adventure directory (including your sounds and images) and upload it to the assignment server.

After everyone has uploaded their assignments, the web site software will anonymously assign everyone a set of games to critique.  You will then download them, run them, and fill in a critique form on line.  That will be the second part of the assignment.  Your critiques will then be sent (again, anonymously) to the authors of their respective games, and you will see the critiques of your game.  You are also free to continue working on your game during this period.

Finally, you will look at the feedback from other students, finish your game, and upload the completed version.

FAQ

Adding a new room to your game

To make a new room, you just:

Adding hotspots to room images

Start by adding the room to the game and specifying the image for the room.  The run the game and go to the room.

That's all there is to it.

Notes:

Adding new Things

To add a thing to your game, you would just choose a name for it (or use null, if you want the object unnamed), and a room to place it in.  Then add the following line to the room's setup procedure:

            [new Thing "name" place]

where name is the name of the object (or null) and place is the name of the variable in which you put the Place object for that room.  You may want to specify the adjectives for the Thing, in which case, you'll need to put the thing in a variable, as in:

            [with mything = [new Thing "name" place]
                [mything.adjectives ← [list "big" "green" "silly"]]]

At some point, you may find yourself needing to write code that manipulates this Thing from outside of the setup routine.  Then you will need to use a global variable rather than a local variable like you would make using with.  So then you need to add another define (outside of the setup routine) to make the global variable and then set it in the setup routine for its room.  So instead of the command above, you'd have something like:

            [mything ← [new Thing "name" place]]
            [mything.adjectives ← [list "big" "green" "silly"]]

with [define mything null] someplace outside of the setup procedure.

Adding new Doors

This is the same as adding new things, except you say:

            [new Door "name" place  destination]

That is, you change the name of the type and you add a second place, the destination.

Of course, there is the problem, as discussed in class, that doors are only one-way.  So you probably want to make pairs of doors, in which case, you may want to steal the make-door-pair procedure from the sample game and use that.

Adding new kinds of Things

If you make all your objects be of type Thing, then it will keep printing out text like "you see a thing name Sam".  If you want it to say "you see a dog named Sam", then you need to create a new class of objects:

            [define Dog
                       [class [Dog name location]
                                 Thing]]

and then change your code from (for example):

            [new Thing "Sam" the-doghouse]

to:

           [new Dog "Sam" the-doghouse]

Adding state information to your objects

Suppose you want to make a new kind of door that can be locked.  Then you need to make a new class, say, LockableDoor, and add a new field to it called locked?

[define LockableDoor
           [class [LockableDoor name location destionation locked?]
                     Door]]

Notice that this class's parent is Door, rather than Thing, so all the fields and methods for Doors (and Portals and Things) are now automatically used for lockable doors.  However, LockableDoors have one extra field, the locked? field.  This doesn't help us, though unless lockable doors only let you pass through when they're unlocked.  How do we do that?  Well, remember from the lecture that the way doors work is that you click on the door or type "go to door" and the system runs the generic procedure called click, and it moves the user through to the destination.  We can change this by defining a new method for click that only runs when the argument is a LockableDoor:

[define-method [click [LockableDoor d]]
    [if d.locked?
        [print-line "Sorry, the door is locked"]
        [call-next-method]]]

Notice that we use call-next-method here when the door is unlocked.  This is because we want the door to do whatever it is that normal doors do.  Rather than looking that up and retyping it, we can simply tell the system to continue on as if the door was a normal Door (or Portal) rather than a LockableDoor.

Of course, you would also need to provide the user with some way of unlocking the door.  There are a lot of different ways of doing that, e.g. by providing the player with special keys, or code numbers, or other puzzles.  One way or another, you need to give them some sort of command (probably using define-pattern) for unlocking the door.

Changing the behavior of your objects

Suppose you want to make a new kind of door that plays a sound when you go through it.  This is even easier than the example above.  We just make a new class, NoisyDoor, and define a new click method for it as above:

[define NoisyDoor
           [class [NoisyDoor name location destination]
                     Door]]

[define-method [click [NoisyDoor d]]
    [play-sound door-sound]
    [call-next-method]]

This makes a new kind of door, NoisyDoor, and defines a new click method for it that starts by playing some sound and then continues on into the normal click method for doors.

Can you make a define-pattern that works differently when you're in different places?

Define-pattern just says what procedure to call when the user types a certain text pattern. So all you have to do is write the procedure to do different things in different places. Note, however, that the system doesn't have notions of north, south, east, and west, or of coordinates of places. So you'd have to modify the system to make it understand that.

How do you make a place display different pictures at different times?

There's a generic procedure called view-image, described in unit 6, that tells what picture to show for a given place. At the moment, it only has one method, which just returns the value of the "image" place object (i.e. p.image, if p is the place object). If you want multiple images, you can either change the image field, or you can make a new subclass of Place and write a new method for view-image for that new subclass. Then it can do whatever you want to decide what image to display. So in other words, you say something like:

[define MyPlaceClass
          [class [MyPlaceClass fields ...]
                    Place]]

[define-method [view-image [MyPlaceClass p]]
    ... insert code to choose image here ...]

Note, however, that the way you tell the system to redraw the image is to the procedure that's handling user input (i.e. the procedure called by define-pattern or the procedure in the hotspot) return the value: true. If the handler returns true, the system redraws the display. If it returns false, it leaves the display alone.

I want to add pockets to my player.  How do I make the system recognize the names of objects in the player's pockets when it sees them in commands?

The way the system finds the objects that you name in the commands you type is through the procedure search-for-object, which is at the end of the file Parsing.meta.  It looks like this:
 

[define search-for-object
          
[test →  [find  test  the-player.location.contents]]]

In other words, the system passes in a test procedure (named, not surprsingly, test), and it uses find to test every object in the contents of the player's current room.  The player's current room is the-player.location, so the contents of the room is the-player.location.contents.

To make the system search other places too, just add their contents to the list being searched.  For example:

 
       [find test
              [append the-player.location.contents
                          pocket.contents]]
 
Where pocket is an expression for the Place object for the player's pocket.  The easiest way to do this is just to have a global variable that is the pocket object.  Then, you'd just say pocket.contents.  The fancy way would be to make the pocket really be part of the player, by making it a field of the player object.  Then you'd say the-player.pocket.contents.

How do you add a new field to the player object?  Read on.

 

How do I add a new field to the player object?

Just open the Player.meta file and add it to the class definition.  Then save the file and do [reload].  For example, if you want to add a pocket to the player object, just change the definition of the Player class from:

[define Player
           [class [Player name location]
                     Actor
                     previous-location]]

to:

[define Player
           [class [Player name location]
                     Actor
                     previous-location pocket]]

Since, in this case, I added the pockets field to last line, which are fields that don't take their values from the arguments in the new call, we probably want to add an initialize method for Player to make a Place object for the pockets field:

[define-method [initialize [Player p]]
    [p.pocket ← [new Place "Pocket"]]]

This says that whenever some new Player object is created, set its pocket field to be a new Place object name "Pocket".