CPSC 240 — OOA&D — Spring 2024

"Zork II"

Due: Wednesday, March 13, 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:

  1. 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.
  2. 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:

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. 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:

  1. First, do dungeon hydration.
    1. 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.)
    2. 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
    3. 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.
    4. 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.
    5. 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.
    6. 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.
    7. 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.
    8. 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.
    9. 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.
    10. 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 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 --- 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.
    11. Verify that your whole program can successfully 'play' this dungeon.
    12. 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
      
    13. Change your hardcoded "../files/simple.zork" reference to "../files/farmer.zork," and verify that your whole program can successfully 'play' farmer.zork.
    14. 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.
  2. Then, do game state persistence.
    1. 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.)
    2. 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".
    3. Add the ability to write the second line of the save file in GameState::store().
    4. Add the rest of GameState::store(), which will write the rest of the file including the ending delimiter.
  3. Finally, do game state hydration.
    1. Write just enough of the GameState::restore() that the whole room section can be skipped over.
    2. 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.
    3. Complete GameState::restore(). Test that this works (now everything should be restored from the previous game.)
    4. Finally, finish up the command-line argument stuff, distinguishing between the two types of files and processing each one appropriately.
  4. 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

  1. Please remove any extraneous System.out.print (or .println()) debugging statements before bundling and submitting.
  2. 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.
  3. 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.
  4. The visibility modifiers in the above class diagram must be implemented correctly in the code.
  5. 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

(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!!!"