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:
- 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").
- 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.
>
- 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.
>
- 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.
>
- 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.
>
- Finally, the q command exits the program. (You may prompt for
confirmation before quitting if you like, though I find this annoying.)
- 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.
- Create a basic main() routine in your Interpreter class
so that it repeatedly prompts the user for input until she enters
"q".
- 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.
- Write the Dungeon class in its entirety.
- Add the buildSampleDungeon() function to Interpreter,
creating a one-room dungeon.
- 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.
- Write the Exit class in its entirety.
- Finish the Room class (adding exits). Enhance your little test
Room::main() routine to test that it all works.
- Celebrate your progress so far by fleshing out your five-room-dungeon-with-exits in
buildSampleDungeon(). Be at least mildly creative.
- Write the GameState class in its entirety. This will involve,
among other things, instantiating a (blank) HashSet object to store
the visited rooms.
- 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.
- 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.
- 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().
- Make sure you can actually play the whole game (i.e., traverse the
whole dungeon, seeing all rooms and exits) from your
Interpreter.
- 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
- Please remove any extraneous System.out.print (or
.println()) debugging statements before bundling and submitting.
- 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.
- The only place you should be instantiating Command objects is from
within your CommandFactory class.
- 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
- 1 extra credit point: at least one (possibly
trivial) commit to your repo before midnight Sep 18th
- 1 extra credit point: at least one
non-trivial commit to your repo before midnight Sep 19th
- 2 extra credit points: non-trivial commits to
your repo on at least four (4) different dates
(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!!!"