Project 5: Adventure

Due: Wednesday, 04 December, 11:59 PM

Partners Allowed

Github Classroom Link: https://classroom.github.com/a/jxByB0rh

Prof. Jed Rembold's alternate instructions


A. Partners Allowed [Show]

A. Partners Allowed [Hide]

If you partner with a student in a Prof. Jed Rembold section, ensure you both use the same link. You can find the Prof. Jed Rembold classroom link here.

Because large projects like this one typically involve more than one programmer, you are encouraged to work on this project in teams of two, although you are free to work individually as well. Each person in a two-person team will receive the same grade.

To allow for the possibility of partners, everyone will need to make a team when accepting the assignment, even if you are just going to be working on things solo. Once one partner has made the team, the other can join it. If you accidentally join a team you did not mean to, let Professor Rembold know, and they can see about getting you removed so that you can join another. Only join an existing team if you intend to work with that person! It can help if you name your teams based on the expected team member’s names.

While most pairs will likely want to work on the code together, in person, there are good resources at your disposal that can help should you would want to work more remotely. If you work in a pair, you will both have access to a shared repository on GitHub. This means that, as long as you are good about uploading your files, each person can usually work on the latest version of the code without too much issue.

There are some built in ways within VSCode that you can sync your local files with GitHub files as well. VSCode also has a remote sharing extension, which can essentially give you collaborative control over your code, similar to Google docs. The extension is called “Live Share” if you want to search for it within the VSCode extensions.

In general, you should check in code frequently when working with partners.


B. Files [Show]

B. Files [Hide]

I think of this project as having three kinds of files - Adventure Python (.py) files, Teaching Machine Python (.py) files, and text (.txt) files.


Files:

  • Adventure Python (.py) files - Do your work in these files
    • Adventure.py - Run this file to test your program
    • AdvGame.py
    • AdvRoom.py
    • AdvItem.py
    • tokenscanner.py - An optional helpful file
  • Teaching Machine Python (.py) files - These are in the TeachingMachine folder
    • TeachingMachine.py
    • TMCourse.py
    • TMQuestion.py
  • Text (.txt) files
    • Various, discussed within milestones as needed.

0. TM -> Adv [Show]

0. TM -> Adv [Hide]

Modify TMCourse and TMQuestion to make AdvGame and AdvRoom

Of the initial 4 Python files for Adventure, most of the work is incomplete. For this milestone, you will make changes to AdvGame.py and AdvRoom.py.

The code provide for the Teaching Machine is complete and can be used as a template. To get started on Adventure, I recommend doing the following:

  1. Within TMCourse.py and TMQuestion.py:
    • Convert all mentions of "TM" to "Adv"
    • I replaced both Capitalized and lowercase terms for:
      • "course" to "game"
      • "question" to "room"
    • Convert all mentions of "answer" to "passage".
    • Convert all mentions of "text" to "long_description".
    • I use "ctrl+h" or "cmd+h" to find and replace these mentions.
  2. I moved the following methods and functions directly from the TM files to the Adv files
    • From TMCourse to AdvGame:
      • run
    • From TMQuestion to AdvRoom:
      • get_name
      • get_passages
      • read_room
  3. For AdvRoom, I updated the code to support short descriptions:
    • I updated __init__ to set self._short_description
    • I wrote the getter methods for short_description
    • I modified the read_room function at the bottom of AdvRoom to provide the argument text twice - once as a short description and once as a long description - so that the __init__ method would have the correct number of arguments.
      • We will deal with short and long descriptions in the next milestone.
  4. I added read_room to the import statement at the beginning of the file with AdvGame - initially the import statement only imports AdvRoom.
    • The Teaching Machine imported first read_course and now read_room, and we will need to use read_room.
  5. I took the code from the TMCourse file function read_game (formerly "read course") and placed it within in the __init__ method of AdvGame.
    • I had to increase the identation level by one.
    • I had to remove the return statement.
    • In lieu of the return statement, I:
      • Created a new attribute, self._rooms
      • Set it equal to dictionary rooms
      • Updated the get_room getter method.
  6. I modified the __init__ method to work with a string that is a prefix to a file name, rather than with a Python file object:
    • In Teaching Machine, a file is opened in TeachingMachine.py and read_course is called with this file as an argument - this is the f referred to in the code, such as in the following line:
      room = read_room(f)
    • In Adventure.py, no such file is opened and instead AdvGame is given a DATA_FILE_PREFIX which is initially set to "Tiny"
      DATA_FILE_PREFIX = "Tiny"
    • This prefix is sole argument to the AdvGame.__init__ method, shown here:
      game = AdvGame(DATA_FILE_PREFIX)
    • So, I took the following code from TeachingMachine.py:
      with open(filename + ".txt") as f:
    • And modified it for AdvGame.
      • I changed "filename" to "prefix"
      • I changed ".txt" to "Rooms.txt", the Adventure game specific suffix for rooms files
      • I placed this with statement in the line immediately prior to room = read_room(f), which I indented under this statement, and left other lines at their original indentation level.
        with open(prefix + "Rooms.txt") as f:
            while not finished:
                room = read_room(f)
            if room is None:

      • I ran Adventure.py and verified I was able to see text prompts.

When you see following when running Adventure.py and providing first "west" then "east" as inputs, you are ready to move on to Milestone 1.


Outside building
You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
> west
End of road
You are at the end of a road at the top of a small hill.
You can see a small building in the valley to the east.
> east
Outside building
You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
>

1. _short_desc [Show]

1. _short_desc [Hide]

Track visits.
  1. Update the read_room function in AdvRoom.py to handle descriptions.
    • The short description is the zero-indexed element of the lists of strings called text.
    • The long description is list slice of all strings after the zero-indexed string of the list of strings called text.
    • short, long = text[0], text[1:]
  2. Update the AdvRoom class to have a visitation tracker.
    • I created a self.visited attribute and initialized it to False
  3. Update the AdvGame.run method to mark rooms as visited after visiting them.
    • I set the room to be "visited" after printing its description.
  4. Update the AdvGame.run method use short or long descriptions.
    • Before printing a description, check if the room was visited.
    • If so, print the short description.
      • Be advised, this may be a string instead of a list of strings depending, probably, on your code in read_room
    • If not, print the long description.
      • There is existing code to print the long description, which is a list of strings.
      • You are welcome to find other ways to store the long description, most likely in read_room
    • I implement this all as a "print_text" method in AdvRoom, but you may do whatever you like!

When you see following when running Adventure.py and providing first "west" then "east" then "west" as inputs, you are ready to move on to Milestone 2:


You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
> west
You are at the end of a road at the top of a small hill.
You can see a small building in the valley to the east.
> east
Outside building
>

2. QUIT, HELP, and LOOK [Show]

2. QUIT, HELP, and LOOK [Hide]

Implementing the QUIT, HELP, and LOOK commands

So far, anything typed into Adventure corresponded to a passage which led to another room. For this milestone, we will check to see if the typed text is "QUIT", "HELP", or "LOOK" and print accordingly.

  • In response to "QUIT", run Python quit() which will cause Python to stop running.
  • In response to "HELP", run print(HELP_TEXT)
  • In response to "LOOK", print the long description of the current room.

The most obvious place I found to place this code was in AdvGame.run. I used an if statement to see if the response was a room or a command.

Because the description of a room is only printed when the room changes, I had to make a few changes to how AdvGame.run worked in general. Basically, check to see if the room has changed before before printing the room description.

I dealt with commands much the way I dealt with buttons in ImageShop - each named command had a corresponding function. I wrote these in AdvGame.

When you see following when running Adventure.py and providing first "west" then "east" then "west" as inputs, you are ready to move on to Milestone 3:


You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
> west
You are at the end of a road at the top of a small hill.
You can see a small building in the valley to the east.
> east
Outside building
> look
You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
> help
Welcome to Adventure!
Somewhere nearby is Colossal Cave, where others have found fortunes in
<many such lines of text>
want to end your adventure, say QUIT.
> quit

3. Items [Show]

3. Items [Hide]

Calvin D. broke this milestone into 5 sub-milestones to help organize the assignment. For this milestone you will want to move from "Tiny" to "Small" prefix in Adventure.py: DATA_FILE_PREFIX = "Small"
Implement items.

3a: read_item in AdvItem.py

Write the function read_item based on your code for read_room. I should note, I found this considerably easier than reading rooms. In all cases there is:

  • A line containing an item name in all caps
  • A line containing a item description
  • A line containing an initial location of the item that is either the name of a room or "PLAYER"
  • A blank line

My read_item either returned None after reading all items or an AdvItem object in the final line...

3b: AdvItem class in AdvItem.py

I implemented the following methods of the AdvItem class:

  • __init__
  • __repr__

You may also want to implement a getter method to find the initial location.

In __repr__, I simply returned the value of the description so that I was able to call print() on AdvItem instances directly, which I found easier than using a AdvItem.get_description() method and printing the returned string.

When you think your AdvItem methods and read_object function are ready, you can test them while adding objects to rooms.

3c: AdvRooms.items attribute in AdvRoom.py

I added a single attribute to the AdvRoom class to track what items are in a room.

I used a dictionary, where keys were the names and the values were the AdvItem objects.


self.items = {}

You will need to be able to add, remove, and check for the presence of any item in a room, which naturally corresponds to three dictionary operations and three methods of the AdvRoom class.

3d: Read items from the appropriate file within AdvGame.__init__

I stored the items within the appropriate room. There is one special case, where the item is attached to the PLAYER, which we ignore for this milestone.

  • Open the items file:
    with open(prefix + "Items.txt") as f:
  • Read items for the file
    • For each item, find the corresponding initial location.
    • Add that item to the set of items within that location.

3e: Print descriptions of items when entering rooms.

Update AdvGame.run to print all items in a room. This should be formatted as the string "There is a" followed by the description of the item (as a string) followed by the string "here.".

You are ready to move onto the next milestone when the set of keys is reported upon entering the relevant room, as seen below:


You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and   
down a gully to the south.  A road runs up a small hill   
to the west.
> in
You are inside a building, a well house for a large spring.
The exit door is to the south.
There is a set of keys here.
> 

4. QUIT, HELP, and INVENTORY [Show]

4. QUIT, HELP, and INVENTORY [Hide]

Item Commands

You previously implemented "QUIT", "HELP", and "LOOK", likely as a series of statements calling helper functions, like "look_action". For this milestone, you will add three new commands: "TAKE", "DROP", and "INVENTORY".

4a: Two-part commands

Each of "TAKE" and "DROP" are special commands that require an additional argument:


You are inside a building, a well house for a large spring.
The exit door is to the south.
There is a set of keys here.
> take keys
Taken.
>

The easiest way I found to work with these multi-word commands was with the string method ".split" which works as follows:


>>> ex = "take keys"
> ex.split()
['take', 'keys']

That is, by using the split method of a string, you will get a list of words. It also works on strings with no spaces, and simply returns a list of length one. I had a simple "if" statement to manage the case where there was an additional argument and I am dealing with a "TAKE" or "DROP".

4b: AdvGame.inventory

In Milestone 2, we ignored the case of items that are held by the player. Add an attribute to AdvGame to keep track of the items the player is currently holding. I used a dictionary named inv, for "inventory".

You will need to update "read_item" to deal with the case where the initial location of an item is "PLAYER".

4c: TAKE item

TAKE checks the current room to see if item is present. If so, the item is (1) removed from the room and (2) added to the inventory you created in 4b. If an item is taken, the game should acknowledge that by printing the text "Taken".

4d: DROP item

DROP checks the current room to see if item is present. If so, the item is (1) removed from the room and (2) added to the inventory you created in 4b. If an item is taken, the game should acknowledge that by printing the text "Dropped".

My code for "TAKE" and "DROP" was virtually identical, and it would be possible to write and helper function that does both, though not necessarily easier depending on your other design decisions.

4e: INVENTORY

This command will list the items in the players inventory, which was implemented in 4b.

  • If there is nothing in inventory:
    • Print "You are empty-handed."
  • If there is at least one item in the inventory:
    • Print "You are carrying: on one line.
    • For each item, print one line:
      • Print two (2) spaces " "
      • Print the description
        • Like "a set of keys"
        • Do not print the name, like "KEYS"

Here is an extended example:


There is a set of keys here.
> inventory
You are carrying:
   a bottle of water
> take keys
Taken.
> inventory
You are carrying:
   a bottle of water
   a set of keys
> drop water
Dropped.
> drop keys
Dropped.
> inventory
You are empty-handed.
>

5. Synonyms [Show]

5. Synonyms [Hide]

Alternative commands

The file with the Synonyms.txt ending contains a series of lines:


N=NORTH
S=SOUTH
...

In each case there is:

  • A "synonym": one of more letters that is not a current command
  • An equals sign "="
  • An existing command or item name,

5a: Read a Synonyms file

Either with in AdvGame.__init__, or more likely within a read_syns function called from AdvGame.__init__, read each line from the appropriate file and populate a dictionary where keys are synonyms and values are commands.

You have sample code from read_item to read and process lines of text.

I highly recommend the use of ".split()" again, but this time with the argument "=" in order to split on equals-sign instead of the default spaces.Here is an example:


>>> ex = "N=NORTH"
>>> ex.split("=")
['N', 'NORTH']
>>>

Create a dictionary in AdvGame that contains all the synonyms as keys and the commands/items as values.

5b: Update run

Use synonyms in AdvGame.run

After reading in a response and possibly breaking it into two, instead of one, words, we use synonyms.

Check each input word, and if it is in the synonyms dictionary, update the response to include the command/item name instead.

This was hard for me to explain - here is an example:


>>> word = "N"
>>> if word in syns:
...     word = syns[word]
... 
>>> word
'NORTH'

Here is an extended example that uses both command ("release" and "i") and item ("bottle") synonyms:


You are standing at the end of a road before a small brick
building.  A small stream flows out of the building and
down a gully to the south.  A road runs up a small hill
to the west.
> release bottle
Dropped.
> i        
You are empty-handed.
>

6. Locked Passages [Show]

6. Locked Passages [Hide]

Reorganize passages to check items

We recall that dictionaries may only store one value per key.

The default code that read a rooms file into a dictionary would have read lines like this, in SmallRooms.txt:


-----
NORTH: SlitInRock
UP: SlitInRock
DOWN: BeneathGrate/KEYS
DOWN: MissingKeys

Unless you changed this considerably, the first "DOWN" option would've been overwritten by the second. In this case, there would be no way to reach the "BeneathGrate" room.

However, now, with the benefit of items, we wish to change rooms conditionally based on whether an item is in inventory (or not).

6a: Update read_room

You will need to update the code in read_room that updates the dictionary of packages. By default, it did not check to see if a certain response was already present in the passage dictionary.

I recommend changing the values in the passage dictionary from strings to lists of strings. When adding things to a dictionary, I was working with three variables:

  1. passages: a dictionary where keys are directional commands and values are names of rooms
  2. reponse: a string that is a directional command (a key)
  3. next_room: a string that is the name of a room
I recommend doing this as follows:
  • Check to see if a response is already in passages.
    • If not, use .split('/') to convert the next_room to a list.
      • If there is no locked passage, the list will be of length one.
      • If there is a locked passage, the list will contain first the appropriate room and then the appropriate key.
    • If so, add "next_room" to the existing list. I added it at the beginning, but that is up to you.

6b: Update AdvRoom.run

Probably AdvGame.run is reading a response from input() and then, checking if the response is a command or not, and if it is not a command checking if it is the name of a room. In my code, this name was used to check for passages, like so:


passages = room.get_passages()
next_room = passages.get(response, None)

However, we have now changed the values within the passages dictionary to be lists instead of strings. Specifically, they are now lists of either the names of rooms or the names of items. Previously, they were all names of rooms.

In brief, next_room (or whatever you named it) is now a list.

I updated AdvRoom to check the length of the next_room list.

  • If the length is one, the value at index zero is the name of the next room.
  • If the length is greater than one:
    • Check to see if the last element of the list, which is the name of an item, is in the inventory. This is a string at index -1
      • If so, the name of the next room is the string immediately prior to the name of the item, in my case the string at index -2 (or index 1).
      • Otherwise, the name of the next room is the string at the beginning of the list, the string at index 0.

Here is an example of how this could work. In this case, I added a single print statement to print the next_room list. These should be removed before going to the next milestone.


> down
['OutsideGrate']
You are in a 20-foot depression floored with bare dirt.
Set into the dirt is a strong steel grate mounted in
concrete.  A dry streambed leads into the depression from
the north.
> down
['MissingKeys', 'BeneathGrate', 'KEYS']
You are in a small chamber beneath a 3x3 steel grate to
the surface.  A low crawl over cobbles leads inward to
the west.
There is a brightly shining brass lamp here.
> i
You are carrying:
   a bottle of water
   a set of keys
> up
['OutsideGrate']
Outside grate
>

In this case, because I had the item KEYS in inventory, when going "down" I went to "BeneathGrate" rather than "MissingKeys".

I removed the print statement printing the list immediately after this test.


7. Forced Movement [Show]

7. Forced Movement [Hide]

Use FORCED to show players messages

We recall a room called "MissingKeys":


NORTH: SlitInRock
UP: SlitInRock
DOWN: BeneathGrate/KEYS
DOWN: MissingKeys

If the player lacks "KEYS" and tries to go down, they go to a missing keys room that that looks like this:


MissingKeys
-
The grate is locked and you don't have any keys.
-----
FORCED: OutsideGrate

In Adventure, when a message must be displayed to the player, a "fake" room is used to show the text, then FORCED places the player where they should be.

For this milestone, we will check to see if there is a FORCED present after updating the room, and if so, print the long description and then progress to the next room.

Note: We will not do this if there is a FORCED present, but while there is a FORCED present, in case there a multiple forces in a row. Be advised, a passage may be both locked (Milestone 6) and forced (Milestone 7).

The code in the while loop is quite similar to the earlier code in AdvRoom.run which checks rooms for passages and passages for rooms.

Here is an example of how this could work. In this case, I added a single print statement when encountering FORCED. This should be removed before submitting your code.


concrete.  A dry streambed leads into the depression from
the north.
> down
DEBUG: Being forced, room name is: MissingKeys
The grate is locked and you don't have any keys.
Outside grate
> down
DEBUG: Being forced, room name is: MissingKeys
The grate is locked and you don't have any keys.
Outside grate
>

In this case, because I lacked the item KEYS in inventory, when going "down" I went to "MissingKeys" then "OutsideGrate" rather than "BeneathGrate".

I removed the print statement immediately after this test.