Github Classroom Link: https://classroom.github.com/a/jxByB0rh
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.
I think of this project as having three kinds of files - Adventure Python (.py) files, Teaching Machine Python (.py) files, and text (.txt) files.
Adventure.py
- Run this file to test your programAdvGame.py
AdvRoom.py
AdvItem.py
tokenscanner.py
- An optional helpful fileTeachingMachine.py
TMCourse.py
TMQuestion.py
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:
TMCourse.py
and TMQuestion.py
:
TMCourse
to AdvGame
:
run
TMQuestion
to AdvRoom
:
get_name
get_passages
read_room
AdvRoom
, I updated the code to support short descriptions:
__init__
to set self._short_description
short_description
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.
read_room
to the import statement at the beginning of the file with AdvGame
- initially the import statement only imports AdvRoom
.
read_course
and now read_room
, and we will need to use read_room
.
TMCourse
file function read_game
(formerly "read course") and placed it within in the __init__
method of AdvGame
.
self._rooms
rooms
get_room
getter method.
__init__
method to work with a string that is a prefix to a file name, rather than with a Python file object:
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)
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"
AdvGame.__init__
method, shown here:
game = AdvGame(DATA_FILE_PREFIX)
TeachingMachine.py
:
with open(filename + ".txt") as f:
"filename"
to "prefix"
".txt"
to "Rooms.txt"
, the Adventure game specific suffix for rooms files
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:
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.
>
_short_desc
[Show]_short_desc
[Hide]Track visits.
read_room
function in AdvRoom.py
to handle descriptions.
text
.
text
.
short, long = text[0], text[1:]
AdvRoom
class to have a visitation tracker.
self.visited
attribute and initialized it to False
AdvGame.run
method to mark rooms as visited after visiting them.
AdvGame.run
method use short or long descriptions.
read_room
read_room
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
>
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.
quit()
which will cause Python to stop running.
print(HELP_TEXT)
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
DATA_FILE_PREFIX = "Small"
Implement items.
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:
"PLAYER"
My read_item
either returned None
after reading all items or an AdvItem
object in the final line...
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.
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.
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.
with open(prefix + "Items.txt") as f:
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.
>
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".
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".
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"
.
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"
.
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.
This command will list the items in the players inventory, which was implemented in 4b.
"You are empty-handed."
"You are carrying:
on one line.
" "
"a set of keys"
"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.
>
Alternative commands
The file with the Synonyms.txt
ending contains a series of lines:
N=NORTH
S=SOUTH
...
In each case there is:
=
"
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.
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.
>
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).
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:
passages
: a dictionary where keys are directional commands and values are names of rooms
reponse
: a string that is a directional command (a key)
next_room
: a string that is the name of a room
.split('/')
to convert the next_room to a list.
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.
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.
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.