CPSC 240 — OOA&D — Fall 2024
"Zork II"
Due: Wednesday, Oct. 9 Friday, Oct. 11, midnight
Zork II: Persistence & hydration
Your next addition to your Zork project will involve persistence and
hydration, two fancy-sounding terms that mean storing and retrieving a
running system's state. This will involve two areas:
- The dungeon design — hydration only. Instead of
hardcoding all the rooms and descriptions and exits inside the Java program
itself (which is unwieldy to say the least, let alone unfriendly to a
non-programmer) you will read the dungeon, and all its components, from a file.
The filename will be specified on the command line when you run Zork, so that
your single game engine will be able to run many different dungeons.
We're going to make this process unidirectional: you'll write code to turn a
.zork file into a Dungeon object, but not the other way around. The
latter would be a job for a "Dungeon Builder" type of program, which would aid
a dungeon designer in designing her world and writing her text. We're not
writing that program this semester.
- The game state — persistence & hydration.
Currently, a user can freely explore your Zork dungeon, but there is no way for
her to save her progress so she can resume later. We'll change that in v2 by
writing progress to a save file and adding the ability to start Zork with a
save file instead of just a dungeon.
The requirements
File format: dungeon file (.zork)
A Zork II Dungeon design will be stored in a file with a .zork
extension, and in the following format. Study it carefully.
James Farmer Hall
Zork II
===
Rooms:
Rotunda
You are in a beautiful round entry chamber, with tall white pillars that
seemingly reach to the skies. There is an elevator here.
---
Basement
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.
---
Back hallway
A smaller, cozier hallway greets your eye. A small kitchen area with a purring
coffee machine is on one side, and on the other, a row of welcoming office
doors. One has Star Wars insignias on it.
---
Stephen's office
This is a cluttered office, with many geeky toys sprawling on a desk. Dr.
Anewalt finds it cold in here.
---
Room 044
Sunlight streams through tall windows and illuminates a jolly little classroom.
The students in it can see you through the door windows. They smile and wave.
---
Rotunda balcony
You stand on a circular white balcony overlooking an entry hall. Columnar
bannisters in ancient Grecian style stand between you and the precipice.
---
===
Exits:
Rotunda
u
Rotunda balcony
---
Rotunda balcony
d
Rotunda
---
Rotunda
d
Basement
---
Basement
u
Rotunda
---
Basement
w
Back hallway
---
Room 044
n
Basement
---
Basement
e
Room 044
---
Back hallway
s
Stephen's office
---
Stephen's office
n
Back hallway
---
Room 044
w
Back hallway
---
Back hallway
e
Room 044
---
===
Reminder: as you may remember from CPSC 220, file I/O is very finicky work
in any programming language. This is because programs are very bad at making
assumptions, reasoning from incomplete information, interpreting what a file
writer "meant" rather than what they wrote, etc. The file format above
must be adhered to religiously by both you (as a writer of a dungeon file) and
your code (as a reader/parser of the file).
To walk you through this:
- The first line is the dungeon's title. It may contain multiple words.
- The second line is informational, giving the Zork version. You'll read
this line and compare it to the literal string "Zork II". If it's an
exact match, you'll merrily read past it and continue. If not, you'll print an
error message indicating that the dungeon file is incompatible with the
current version of Zork, and exit gracefully.
- The third line is the delimiter "===". Conceptually, such
a delimiter isn't really needed here (since it will always be exactly the
third line that contains it, hence it doesn't give any information the parser
didn't already have), but I think it's nice for aesthetic reasons, so I added
it. Note that since I did decide that such a delimiter should be
part of the dungeon file format here, you must include it in your own
.zork files, and expect it in your parsing code. You'll simply read it and
throw it away.
- The fourth line is the literal string "Rooms:", also nice
aesthetically, also to be thrown away.
- Each room of the dungeon then follows (order doesn't matter; but the
first room in the file will be considered the dungeon's entry room),
with an exactly-one-line room name, and a possibly-multi-line description. A
room entry ends with a "---" delimiter. Observe that in this case,
such a delimiter is conceptually necessary (i.e., I had no
choice but to add one) because without it, your program would have no way of
knowing that the end of the room description had been reached.
- The room section ends when a "===" delimiter follows the last
room entry's "---" delimiter. You might find the back-to-back
delimiter lines aesthetically displeasing, but you'll find it easier to parse
this way. (Aside: I predict some people screw this up, and forget the
"---" immediately before the "===". We'll see if I'm
right.)
- Finally, the exits appear (in any order). Each exit consists of three
lines: the "source" room's name, the direction you would take from that
source room in order to leave by that exit, and the "destination" room's
name. A "---" delimiter ends each.
- Finally, the entire file ends with a "===" delimiter. (Again,
this choice of mine will make it a bit easier for you to parse .zork
files.)
Any file that does not adhere strictly to this format is an illegal
.zork file and should be rejected by your program.
To reiterate: your program will not be writing files in this format. It
will be reading files in this format. The way the .zork
dungeon files will come into existence is that a human (you) will be
hand-writing them using vim.
File format: save file (.sav)
When the user saves her progress, a file will be created with a
.sav extension, in the following format:
Zork II save data
Dungeon file: /home/stephen/teaching/240/zork/files/farmer.zork
Room states:
Stephen's office:
beenHere=true
---
Basement:
beenHere=true
---
Rotunda:
beenHere=true
---
===
Current room: Stephen's office
Details:
To be clear: when the user requests a save, your program should write a file
in this format. It should also read a file in this format when it is
started up (see below).
Startup
Okay, that's what the files will look like. Now for the requirements.
When Zork II starts up, it will accept a single command-line
argument (see section 8.6 if you need a refresher on how these work). This argument will be either (a) the name of a dungeon file, or
(b) the name of a save file. Your program will determine which one it is by
inspecting it to see whether it has a .zork or a .sav
extension.
If it is a .zork dungeon file, your program will load the dungeon
and start the adventurer 'from scratch' in the dungeon's entry room. (Recall
that the entry room will be the first room listed in the file's
"Rooms:" section.) If it is a .sav save file, your program
will restore the game to the exact state it was in when that file was saved.
This will include loading the dungeon itself (listed on the second line of the
.sav file; see above) and then restoring the system state. The
methods to do all this are described below.
If no command-line argument is present, or if it's present but doesn't end
in either .zork or .sav, your program should print a usage
message ("Usage: Interpreter zorkFile.zork|saveFile.sav.") and quit.
(To clarify: the pipe ("|") sign in this usage message is not
literal! It's a convention in Linux command-line documentation that means
"or". The user will not type both a .zork filename and
a .sav filename separated by a |. Instead, the user will
type either a .zork filename (to start a dungeon anew)
or a .sav filename (to continue their progress in a
previously-started dungeon).)
The "save" command
Finally, the user will now have available to him a "save" command,
which will save the game's state to a .sav file. When the user types
"save" you should prompt for the name of a save file. You should
append a ".sav" extension to whatever the user types here, and save
that to a file in the current directory. (For example, let's say the user types
"save" and your program prints "Enter the name of your save
file:", to which the user responds "madeItToRainbow". You should
then save the user's progress to a file called "madeItToRainbow.sav"
in the current directory.)
The design
Here's the v2 UML class diagram:
There are several changes to the Zork I classes to make this all work.
Here's a summary:
- Dungeon (public)
- This class will undergo several changes.
A new constructor will take a .zork filename as an argument. It will
use standard java.io classes to instantiate a Scanner
object based on that filename, so that it can read from the file.
Note: this constructor will only contain code to read information
pertaining to the Dungeon object itself. It will "pass the buck" to
the other classes (Room and Exit) by passing the
Scanner object as an argument to their constructors. This
may be the hardest part of Zork II for you to wrap your head around. It is an
example of good encapsulation and separation of design concerns — the
Dungeon class has no business knowing how a Room or
Exit object is represented on disk, and no business doing their work
for them.
Finally, any Dungeon object that has been instantiated from a file
needs to hold on to its filename, so that it can persist this as part of its
state.
- Room (public)
- Like Dungeon, the Room class will know how to hydrate
itself from the corresponding portion of a .zork file using its new
constructor.
Now consider this. How is the end of the "Rooms:" section of the file
detected and communicated? Answer: the Room constructor, if given a
Scanner object that is "out of rooms" (i.e., it has
been read from enough times that it is no longer positioned at the start of a
new room entry), will throw a NoRoomException exception (a one-line
class of your own designing) back to the caller (which will normally be the
Dungeon constructor). It is up to the Dungeon constructor to
expect and catch this exception, moving on to the "Exits:" section
when it does.
- Exit (public)
- Like Dungeon and Room, the Exit class will have
a new constructor for hydration. As with the new Room constructor, an
exception should be thrown (NoExitException this time) when the end of
the section is reached.
- Command
- "save" is a new command type of sorts, and needs to be accounted
for in the .execute() method.
- GameState
- In addition to being able to .initialize() itself from scratch
with a brand-new Dungeon, the GameState class now must also be able
to .store() and .restore() itself to/from disk.
- Interpreter
- Finally, the main() function must process the command-line
arguments appropriately.
Recommended sequence
Here's my suggestion for where to begin:
- First, do dungeon hydration.
- Write the one-line IllegalDungeonFormatException,
IllegalSaveFormatException, NoRoomException, and
NoExitException classes. (If you wish, you can declare these
inside your Dungeon, GameState, Room and
Exit classes, respectively, so that they don't need a whole
.java file all by their lonesomes. They're then technically called
"Dungeon.IllegalDungeonFormatException",
"GameState.IllegalSaveFormatException",
"Room.NoRoomException", and "Exit.NoExitException" and
should be referred to as such.)
- Create a "files" subdirectory under your main zork
project directory, and create a simple, truncated simple.zork file in
that directory with just these contents:
Simple Dungeon
- Write enough of the Dungeon constructor so that it can open a
file, read the dungeon's title, set its title instance variable,
and return.
- Forget about command-line arguments for now: just have your
main() declare a Dungeon object with the
"../files/simple.zork" filename hardcoded outright. Test that this
much works and that you can successfully instantiate this Dungeon
object and call .getTitle() on it.
- Add a line to your truncated simple.zork file:
Simple Dungeon
Zork II
Verify that you can read and compare the version line as described above,
and that you throw an IllegalDungeonFormatException if it's not
correct.
- Add a single, one-line room to your simple.zork file:
Simple Dungeon
Zork II
===
Rooms:
Boring Room
This is a room with plain, white walls.
---
and write enough of the Dungeon and Room constructors to
hydrate it. At this point, just have your Room constructor assume
that the description is a single line.
- Expand this to a multi-line description:
Simple Dungeon
Zork II
===
Rooms:
Boring Room
This is a room with plain, white walls.
Some might call it boring, but I call it "home."
I mean, it *does* have a multi-line description!
---
and enhance your Room constructor to deal with multiple lines.
- Expand this to have multiple rooms:
Simple Dungeon
Zork II
===
Rooms:
Boring Room
This is a room with plain, white walls.
Some might call it boring, but I call it "home."
I mean, it *does* have a multi-line description!
---
Tedious Room
This room is missing something, I just can't put my
finger on it.
---
Blah Room
Sooooo sick of quarantine.
---
===
Enhance your Dungeon and Room constructors to deal
with multiple lines, and to detect the end of the "Rooms" section of the
file.
- Now add a single exit to simple.zork:
Simple Dungeon
Zork II
===
Rooms:
Boring Room
This is a room with plain, white walls.
Some might call it boring, but I call it "home."
I mean, it *does* have a multi-line description!
---
Tedious Room
This room is missing something, I just can't put my
finger on it.
---
Blah Room
Sooooo sick of quarantine.
---
===
Exits:
Boring Room
e
Tedious Room
---
Write your Exit constructor, and enhance your Dungeon
constructor, to hydrate this exit.
- Now add multiple exits:
Simple Dungeon
Zork II
===
Rooms:
Boring Room
This is a room with plain, white walls.
Some might call it boring, but you call it "home."
I mean, it *does* have a multi-line description!
---
Tedious Room
This room is missing something, you just can't put your
finger on it.
---
Blah Room
You've just finished Netflix. Next?
---
===
Exits:
Boring Room
e
Tedious Room
---
Tedious Room
w
Boring Room
---
Tedious Room
e
Blah Room
---
Blah Room
w
Tedious Room
---
===
and enhance your Exit and Dungeon constructors to hydrate
them.
- Verify that your whole program can successfully 'play' this
dungeon.
- Download
my farmer.zork
dungeon into your files subdirectory, by cd'ing into
files and then issuing the following command:
$ wget http://stephendavies.org/cpsc240/farmer.zork
- Change your hardcoded "../files/simple.zork" reference to
"../files/farmer.zork," and verify that your whole program can
successfully 'play' farmer.zork.
- Adapt Interpreter's main() to accept a command-line
argument for the dungeon file name, and to print the usage message if it
doesn't get one. Make sure you can type this:
$ java Interpreter ../files/farmer.zork
and successfully load that dungeon.
- Then, do game state persistence.
- Write GameState::store(), writing to a hardcoded filename and
writing only the first line and last line of the file. (Make sure to
.close() the file when you're done.)
- Add the ability to process "save" commands (to the
Command::execute() method). Run this code and make sure that it
does in fact create a two-line save file when the adventurer types
"save".
- Add the ability to write the second line of the save file in
GameState::store().
- Add the rest of GameState::store(), which will write the rest
of the file including the ending delimiter.
- Finally, do game state hydration.
- Write just enough of the GameState::restore() that the
whole room section can be skipped over.
- Test that this works. The effect should be that when the .sav
file is restored (hardcode its name and this behavior for now), the user
resumes play in their correct room, but all memory of which rooms have
been visited is lost.
- Complete GameState::restore(). Test that this works (now
everything should be restored from the previous game.)
- Finally, finish up the command-line argument stuff, distinguishing
between the two types of files and processing each one appropriately.
- Oh, and now delete your buildSampleDungeon() function from the
Interpreter class entirely! (You won't need this anymore.)
Turning it in
You will turn this assignment in by sending an email to
cpsc240submissions@gmail.com with subject line "CPSC 240 Zork II
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.
Don't forget
- Please remove any extraneous System.out.print (or
.println()) debugging statements before bundling and submitting.
- The only places you should have any System.out.print() (or
.println()) commands is your Interpreter class, and in your
Command::execute() method when you prompt for a save filename.
- The Dungeon, Room, and Exit constructors must
work collaboratively to load a dungeon from a file. Do not, for
instance, try to write all the code for hydration in the Dungeon
class. Each class should be responsible for knowing how its section of the
file is formatted, and how it can be parsed to produce the corresponding type
of object.
- The visibility modifiers in the above class diagram must be implemented
correctly in the code.
- The only place you should be instantiating Command objects is from
within your CommandFactory class.
I'll be checking for all 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 Oct 1st
- 1 extra credit point: at least one
non-trivial commit to your repo before midnight Oct 2nd
- 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!
Come to office hours, or send email with subject line "CPSC 240 Zork
HELP!!!"