CPSC 240 — OOA&D — Fall 2024

"Zork I"

Due: Saturday, Sep. 28, midnight

Zork I: Basics

In this project, you will build the basic scaffolding for your Zork text adventure game engine. The result will be a program which actually runs and which a player can interact with, but it won't do much yet: the user will only be able to move around and read room descriptions. You will progressively build on this project a lot in future weeks, as you add more and more features.

The requirements

Your Zork I (version 1.0) program will accomplish the following:

  1. When the program is run, the user will be welcomed to a sample dungeon (of your own devising; see below) and placed in the first room (the "entry").
  2. Each time the user visits a room for the first time, the name of the room and its description will appear, followed by a list of all the available exits from that room. Each subsequent time they visit a room, only the name of the room and the exits will appear — not the description. The first thing the user will see is something like this:
    Welcome to James Farmer Hall! Rotunda You are in a beautiful round entry chamber, with tall white pillars that seemingly reach to the skies. There is an elevator here. You can go u to Rotunda balcony. You can go d to Basement hallway. >
  3. The user has available to her only the six direction commands (n, w, e, s, u, and d), and q ("quit"). Typing anything else should result in a friendly error message and a reprompt:
    > scream I'm sorry, I don't understand the command 'scream'. Rotunda You can go u to Rotunda balcony. You can go d to Basement hallway. >
  4. If the user tries to move in a direction where there is no exit, similar behavior will occur:
    > w Sorry, you can't go w from Rotunda. Rotunda You can go u to Rotunda balcony. You can go d to Basement hallway. >
  5. Moving in a legal direction takes the user to a new room:
    > d Basement hallway A long, white hallway stretches to the east and west. It is cold here, and you can detect the faint smell of body odor. A vending machine hums softly in the corner. You can go u to Rotunda. You can go w to Slopy hallway. You can go e to Bathroom hallway. >
  6. Finally, the q command exits the program. (You may prompt for confirmation before quitting if you like, though I find this annoying.)
  7. Part of your requirements for this project is a simple hardcoded dungeon of at least five rooms, at least one of which must have multiple exits. ("Hardcoded" means that your program — specifically the Interpreter::buildSampleDungeon() method — will use class constructors explicitly with literal values to assemble the components of a Dungeon object that is always the same whenever the program runs. This will change in future weeks.)

The design

I will be giving you the OO design I want you to use for Zork. Please do adhere to it, to the letter. The main reason for doing this is that in order for you to acquire the skill of producing quality designs, you need to first see examples of good designs so you can recognize what they're like. The other reason is that I want you and your future teammates to be on exactly the same page when the Zork team project commences in a few weeks. Hence I want everyone to follow the same architecture. Don't worry, there will be time for open-ended creativity in the second half of the semester, as you and your team evolve Zork into Zork++ in innovative ways.

Here's the Zork I UML class diagram:

As you can see, you will have seven class files. If seven seems like a lot to you, let me say two things. (1) Actually it isn't. Your final Zork++ program may have three or four times that many, and in general, most programs with any degree of usefulness require many classes. You may need to recalibrate your thinking on that. (2) As I emphasized in lecture, one of the chief hallmarks of object-oriented programs is that they have lots of little objects all collaborating together, each one doing its own thing well, rather than one or two huge monolithic code chunks. This characteristic is much of what makes OO programs more versatile, extensible, and maintainable. Perhaps the single most important aspect of this course is for you to learn to prefer this approach.

The classes

Here is a brief description of what each of your seven classes will do.

The first three classes have public visibility: they are designed to be available outside the package, perhaps by a different, "Dungeon Designer" program that assists a game author in interactively building her dungeon. The last four classes are non-public (shown on the diagram with package visibility). They are specific to the workings of the Zork game engine itself, and are not designed to be seen or used by anything outside it.

Dungeon (public)
At this point, nothing but a simple container for a bunch of rooms. It holds on to a collection of Room objects, and knows which one is the entry point. The only possibly confusing thing about this class is that the "collection" should be a Hashtable rather than an ArrayList. A Hashtable is a class that makes it easy to look up entries by a "key" rather than by a numbered index, as an ArrayList does. See Blueprints section 8.5 for lots of details. It turns out we will want to do this kind of key-lookup for rooms: given the name of a room, look up and return its corresponding Room object.

Note carefully — since this has been a point of confusion in the past — that in my lingo a "dungeon" means "the entire world the adventurer plays in, which consists of a number of 'rooms.'" In particular, a "Dungeon" is not one of the rooms itself. (This is standard D&D lingo.)
Room (public)
Each room in the dungeon is represented by a Room object, which knows its name and description. It also knows how to .describe() itself, returning a string of text that is of appropriate length (i.e., it only includes the description if the user is new to the room. It will cooperate with the GameState class to accomplish this.) A Room also maintains a list of Exits, each of which is associated with a direction. (You can use either an ArrayList or a Hashtable to store these Exit objects; either way works.) A description of each exit will be included in the .describe() output, as explained above. (This will change in future versions, but for now it helps debugging and testing.) The .leaveBy() method will return the Room object reachable by the exit in that direction, if any. (Just return null if there is no exit in that direction.)

Note that calling .describe() should itself be enough to have the side effect of marking the room "visited." (See GameState, below.)

By the way, a "room" is simply "any place the adventurer can go." (It does not have to be an indoor room with four walls and a ceiling.)
Exit (public)
A connection between rooms. Each Room object holds on to an ArrayList (or Hashtable) of the Exits from it, and each Exit holds on to the Room that it leads to. An Exit can also .describe() itself, which generates and returns a "You can go d to Basement hallway" type of message.

By the way, realize that "an exit" is unidirectional. It goes from room A to room B in direction C. If you intend the player to be able to return from room B back through that same pathway to room A, then you'll need to instantiate a second Exit object that goes in the other direction. (Example: suppose you want the player to be able to go "s" from the Back Hallway to Stephen's Office, and also "n" from Stephen's Office to the Back Hallway. This implies the existence of two Exit objects, one with its dir inst var set to "s" and owned by the "Back Hallway" Room object, and the other with its dir set to "n" and owned by the "Stephen's Office" Room object.)
CommandFactory
A factory class whose purpose is to parse text strings and produce the appropriate Command objects. Zork I will only have a single type of Command object, and so the CommandFactory may not seem useful. But in Zork II and beyond, the user will be able to do more than just move around, and each thing they're able to do will affect the game in a different way. Hence there will be specialized versions ("subtypes") of Commands and the CommandFactory will have more work to do. (See chapter 13 of Blueprints for a sneak preview of what's ahead.)

Also, the CommandFactory is a Singleton class, which means that there will only ever be one object of that type, and it will be accessed by other classes via the static instance() method, as described in Chapter 7 of Blueprints.
Command
Objects of type Command represent (parsed) commands that the user has typed and wants to invoke. For now, the only type of command is a move command corresponding to one of the directions: when executed, it should update the GameState with the adventurer's new room, if any. (Hint: this will require getting the current room from the GameState and calling .leaveBy() on it.) .execute() should return the text that should be printed to the user in response to that command being executed. (Eventually, this will include messages like "Amulet taken." and "The dragon laughs at your pitiful attempt to attack it." but for now it can just return the result of Room::describe(), since that's all you print in response to movement commands.)
GameState
This too is a Singleton class with an instance() method. It may be the hardest of the seven to wrap your head around. The GameState object simply represents "the current state of the game," which means the information pertaining to the status of the user's experience at a certain point in time. For now, all it knows is (1) which dungeon is being played, (2) what room the adventurer is currently in, and (3) which rooms the adventurer has already visited. When the GameState is initialized with a Dungeon, it should set the current room to be the dungeon's entry point.
Interpreter
Finally, this is the main() class that directs operations. For now, it should create a new hardcoded Dungeon, initialize the GameState with it, and repeatedly prompt the user for input. Each time the user inputs a command, it should use the CommandFactory to instantiate a new Command object and execute it. If the user enters "q", it terminates the program.

Important: because it is the (only) interface between your Zork game and the user, the Interpreter class is the only class that should contain any System.out.print() (or .println()) statements.

Recommended sequence

If you're scratching your head as to where to start, here's a list with my recommended sequence. By the way, after completing each step, do a git commit to snapshot your progress to that point.

  1. Create a basic main() routine in your Interpreter class so that it repeatedly prompts the user for input until she enters "q".
  2. Write everything for the Room class except for exits (temporarily ignore everything to do with exits, including the .leaveBy() and .addExit() methods). For now, make your .describe() method always return the full description of the room, including the name, description, and list of exits. Write a little main() (directly in the Room class is fine) to test that it works.
  3. Write the Dungeon class in its entirety.
  4. Add the buildSampleDungeon() function to Interpreter, creating a one-room dungeon.
  5. Wire your sample dungeon into the rest of Interpreter so that when you run the program, it welcomes you to the dungeon of the proper title, and prints the description of the only room.
  6. Write the Exit class in its entirety.
  7. Finish the Room class (adding exits). Enhance your little test Room::main() routine to test that it all works.
  8. Celebrate your progress so far by fleshing out your five-room-dungeon-with-exits in buildSampleDungeon(). Be at least mildly creative.
  9. Write the GameState class in its entirety. This will involve, among other things, instantiating a (blank) HashSet object to store the visited rooms.
  10. Go back to your Room class and enhance the .describe() method as follows: the first time .describe() is called on a Room object, it should return the name of the room and the full description concatenated together (perhaps with a line break between, for beauty's sake) plus the list of exits. It should also pass itself to the .visit() method of the GameState singleton so that GameState knows the room has been visited. Then any future time .describe() is called on that same room, it should return only the room's name (not the other stuff). Make sure this all works.
  11. Write the Command class in its entirety. Note that for Zork I, a Command object will be given a "direction" in its constructor, save it as an instance variable, and then attempt to move in that direction when its .execute() method is called.
  12. Add Command objects (directly) to your Interpreter::main(). This will be changed momentarily, when you use the CommandFactory instead. For the moment, however, get it working by explicitly instantiating Command objects directly in main().
  13. Make sure you can actually play the whole game (i.e., traverse the whole dungeon, seeing all rooms and exits) from your Interpreter.
  14. Finally, write the CommandFactory class and stitch that in to your main(). Make sure it all still works as before. At this point, the only class that should instantiate Command objects is the CommandFactory class, not the Interpreter class.

Suggested workflow

I suggest you get started with:

$ cd ~
$ mkdir zorkProject
$ cd zorkProject
$ git init .
$ mkdir src
$ cd src
$ vim Interpreter.java   (see below)
$ git add Interpreter.java
$ git commit -a

Your first version of Interpeter.java can look like this:

class Interpreter { public static void main(String args[]) { System.out.println("This will soon be Zork I!"); } }

Also, I recommend adding these lines to your vim ~/.bashrc:

...other stuff in .bashrc... alias gozork="cd ~/zorkProject/src ; pwd" alias runzork="javac *.java && java Interpreter"

And of course, don't forget to:

$ . ~/.bashrc

to source these additions (i.e., make them take effect in the current shell).

Turning it in

You will turn this assignment in by sending an email to cpsc240submissions@gmail.com with subject line "CPSC 240 Zork I turnin" and with your git bundle attached.

Recall that to create a git bundle, first make sure that all your code is checked into git. Running "git status" and ensuring your workspace is clean is a great way to do that. Then, bundle up your git repo:

$ git bundle create yourUmwUsername.git --all

(Please do not name it literally "yourUmwUsername.git" Substitute your actual UMW username. For instance, "jsmith7.git".)

Next, download the yourUmwUsername.git file from your Google Cloud instance to your own machine.

To receive full credit, I must be able to type the following commands in sequence, verbatim, with no variations, to run your program:

$ git  clone  the-name-of-the-repo-you-sent-in-your-email.git  testme
$ cd  testme/src
$ javac  *.java
$ java  Interpreter

If I have to do any extra fiddling because these four commands, verbatim, in sequence, did not work, it's coming out of your grade.

Hint: test that this works before submitting your assignment. You can test it by creating a little temporary practice directory in your home directory:

$ mkdir  ~/pretendImStephen
$ cp  yourUmwUsername.git  ~/pretendImStephen
$ cd  ~/pretendImStephen

and then typing those four commands, above, to make sure they have the desired effect. (You can then get rid of that directory if you want to by typing "cd .." followed by "rm -rf ~/pretendImStephen".)

Don't forget

  1. Please remove any extraneous System.out.print (or .println()) debugging statements before bundling and submitting.
  2. The visibility modifiers in the above class diagram must be implemented correctly in the code; for example, Dungeon should be a public class, but GameState must not be; Room::describe() should have package-level visibility while Room::addExit() should be public, Dungeon::buildSampleDungeon() should be private, etc.
  3. The only place you should be instantiating Command objects is from within your CommandFactory class.
  4. The only place you should have any System.out.print() (or .println()) commands is your Interpreter class.

I'll be checking for all four of these things when I grade your program.

Bonus: starting early and often

(Note: the definition of "non-trivial" is "whatever Stephen deems is non-trivial.")

I need help!

Send email with subject line "CPSC 240 Zork HELP!!!"