Part 9: Refactoring#

In this section we are going to work on refactoring our game. Refactoring is when you change code to make the code better, but without changing what the software does.

It is key to make changes in small, incremental steps which are tested regularly to ensure the program works properly throughout. Avoid tearing out and reworking vast swaths of code at a time, which will often leave you with a hopeless tangle of broken code.

Here we’ll be focusing on the DRY1 principle–don’t repeat yourself. This means that when you notice you’re repeating the same, or nearly the same, code over and over again, find a way to put it somewhere it can be reused. In this case we’ll be adding a few functions, then call those functions in the places where the same code is repeated.

Part 9.1: Add abort()#

The abort() function will be similar to the error() function, except it will exit the game immediately. This function will be for errors that only happen if there is a problem with the code, as opposed to errors that can be caused by something the user typed in.

(This will actually result in a minor change in behavior. Now, when you encounter any errors using the abort() function, your game will end immediately instead of continuing on.)

A. Define abort()#

The abort() function will simply call error() to print an error message, then exit the program.

When abort() is called, a return statement will no longer be needed since the program will end immediately.

  1. [ ] define an abort() function that takes one argument message

  2. [ ] in abort()

    • [ ] call error() with the argument message.

    • [ ] call the built-in exit() function and pass it the argument 1

Code
113def abort(message):
114    """Print a fatal error message then exit the game."""
115    error(message)
116    exit(1)

B. Modify do_take()#

Since we always expect to be able to find an item in the ITEMS dictionary for a key in the place items list, if we fail to find one that means that we messed something up, not the player. This is a perfect place to use abort() instead of error().

Then because abort() exits the program immediately, we can remove the return.

  1. [ ] Call abort() instead of error() when you check if item is falsy

  2. [ ] remove the return statement

  3. [ ] To test, temporarily change the key for "book" to something else, then type take book from home. It should print an error message then exit the program. After verifying that it works, change it back.

Code
270def do_take(args):
271    """Pick up an item and add it to inventory."""
272    debug(f"Trying to take: {args}")
273
274    # make sure the player typed an item
275    if not args:
276        error("What do you want to take?")
277        return
278
279    # get the item name from arguments
280    # and make it lowercase
281    name = args[0].lower()
282
283    # look up where the player is now
284    place_name = PLAYER["place"]
285    place = PLACES[place_name]
286
287    # make sure the item is in this place
288    if name not in place.get("items", []):
289        error(f"Sorry, I don't see a {name!r} here.")
290        return
291
292    # get the item information
293    item = ITEMS.get(name)
294
295    # make sure the item is in the ITEMS dictionary
296    if not item:
297        abort(f"Woops! The information about {name!r} seems to be missing.")
298

C. Modify do_examine()#

This is nearly exactly the same as the previous section.

  1. [ ] Call abort() instead of error() when you check if name is not in ITEMS

  2. [ ] remove the return statement

  3. [ ] To test, temporarily change the key for "book" to something else, then type take book from home. It should print an error message then exit the program. After verifying that it works, change it back.

Code
192def do_examine(args):
193    """Look at an item in the current place."""
194
195    debug(f"Trying to examine: {args}")
196
197    # make sure the player said what they want to examine
198    if not args:
199        error("What do you want to examine?")
200        return
201
202    # look up where the player is now
203    place_name = PLAYER["place"]
204    place = PLACES[place_name]
205
206    # get the item entered by the user and make it lowercase
207    name = args[0].lower()
208
209    # make sure the item is in this place or in the players inventory
210    if not (name in place.get("items", []) or name in PLAYER["inventory"]):
211        error(f"Sorry, I don't know what this is: {name!r}.")
212        return
213
214    # make sure the item is in the ITEMS dictionary
215    if name not in ITEMS:
216        abort(f"Woops! The information about {name} seems to be missing.")

D. Modify do_go()#

Similar to the previous two sections, we always expect to be able to find an place in the PLACES dictionary for a key from another place dictionary, so if we don’t it means there’s an error somewhere in the code.

  1. [ ] Call abort() instead of error() when you check if new_place is truthy

  2. [ ] remove the return statement

  3. [ ] To test, temporarily change the value for home["east"] to something else, then type go east from home. It should print an error message then exit the program. After verifying that it works, change it back.

Code
224def do_go(args):
225    """Move to a different place"""
226    debug(f"Trying to go: {args}")
227
228    # make sure the player included a direction
229    if not args:
230        error("Which way do you want to go?")
231        return
232
233    # get the direction from arguments
234    # and make it lowercase
235    direction = args[0].lower()
236
237    # make sure it's a valid direction
238    if direction not in ('north', 'south', 'east', 'west'):
239        error(f"Sorry, I don't know how to go: {direction}.")
240        return
241
242    # look up where the player is now
243    old_name = PLAYER["place"]
244    old_place = PLACES[old_name]
245
246    # look up what is in that direction from here
247    new_name = old_place.get(direction)
248
249    # print an error if there is nothing in that direction
250    if not new_name:
251        error(f"Sorry, you can't go {direction} from here.")
252        return
253
254    # look up the place information
255    new_place = PLACES.get(new_name)
256
257    # this should never happen if we write the code correctly
258    # but just in case there is no key in PLACES matching
259    # the new name, print an error
260    if not new_place:
261        abort(f"Woops! The information about {new_name} seems to be missing.")
262

Part 9.2: Add get_place()#

Since we’re so often needing to get place information from the PLACES dictionary, we’ll wrap this functionality into a function called get_place() that we can call from anywhere in our program where we need to get place information.

A. Define get_place()#

The get_place() function will take one argument, the key to get from the PLACES dictionary. We’ll make that argument optional though, and set the default to None.

If the user passes a key argument, then we’ll get the place information from PLACES for that key. If they don’t, we’ll get the key from the PLAYER dictionary for their current location. That way we can call get_place() with no args to get the current place.

This function will also check to make sure the place is found in the PLACES dictionary, and call abort() if it is not. That means that anywhere we call get_place() will not have to do that error handling over and over.

  1. [ ] define a get_place() function that takes one optional argument key with a default value of None

  2. [ ] if key is falsy then assign key to the value of the PLAYER dict associated with the "place" value

  3. [ ] get the value from the PLACES dictionary associated from the key key and assign it to the variable place

  4. [ ] If place is falsy,

    • [ ] Use the abort() function to print an error message like:

      "Woops! The information about the place name seems to be missing."

  5. [ ] return place

Code
120def get_place(key=None):
121    """Return the place information from the PLACES dictionary, either
122       associated with key, or if none is passed, where the user is
123       currently at. """
124    # get the current player's place if key is not passed
125    if not key:
126        key = PLAYER["place"]
127
128    # get the place info
129    place = PLACES.get(key)
130
131    # this should never happen if we write the code correctly
132    # but just in case there is no key in PLACES matching
133    # the new name, print an error
134    if not place:
135        abort(f"Woops! The information about {key!r} seems to be missing.")
136
137    return place

B. Modify do_go()#

  1. [ ] Call get_place() with no arguments to get the value for old_place. (This will replace the existing PLACES[old_place].)

  2. [ ] Remove the line assigning old_name since that is taken care of in get_place()

  3. [ ] Call get_place() with the argument new_name to get the value for new_place. (This will replace the existing PLACES.get(new_place).)

  4. [ ] Remove the lines that calls abort() if new_place is falsy.

Code
244def do_go(args):
245    """Move to a different place"""
246    debug(f"Trying to go: {args}")
247
248    # make sure the player included a direction
249    if not args:
250        error("Which way do you want to go?")
251        return
252
253    # get the direction from arguments
254    # and make it lowercase
255    direction = args[0].lower()
256
257    # make sure it's a valid direction
258    if direction not in ('north', 'south', 'east', 'west'):
259        error(f"Sorry, I don't know how to go: {direction}.")
260        return
261
262    # look up where the player is now
263    old_place = get_place()
264
265    # look up what is in that direction from here
266    new_name = old_place.get(direction)
267
268    # print an error if there is nothing in that direction
269    if not new_name:
270        error(f"Sorry, you can't go {direction} from here.")
271        return
272
273    # look up the place information
274    new_place = get_place(new_name)
275
276    # move the player to the new place
277    PLAYER["place"] = new_name
278
279    # print information about the new place
280    header(f"{new_place['name']}")
281    wrap(new_place["description"])

C. Modify do_look()#

  1. [ ] Replace the existing value for place with a call to get_place().

  2. [ ] Remove the line assigning place_name since that is taken care of in get_place()

  3. [ ] Replace the existing value for destination with a call to get_place() with the argument name.

Code
159def do_look():
160    """Look at the current place"""
161
162    debug("Trying to look around.")
163
164    # look up where the player is now
165    place = get_place()
166
167    # print information about the current place
168    header(f"{place['name']}")
169    wrap(place["description"])
170
171    # get the items in the room
172    items = place.get("items", [])
173
174    if items:
175
176        # for each of the place items
177        # get the info from the ITEMS dictionary
178        # and make a list of item names
179        names = []
180        for key in items:
181            item = ITEMS.get(key)
182            names.append(item["name"])
183
184        # remove the last name from the list
185        last = names.pop()
186
187        # construct a sentence that looks like one of:
188        #   x, x and y
189        #   x and y
190        #   y
191        text = ", ".join(names)
192        if text:
193            text += " and "
194        text += last
195
196        # print the list of items.
197        print()
198        wrap(f"You see {text}.\n")
199
200    # add a blank line
201    print()
202
203    # print what is in each direction from here
204    for direction in ("north", "east", "south", "west"):
205        name = place.get(direction)
206        if not name:
207            continue
208
209        destination = get_place(name)
210        write(f"To the {direction} is {destination['name']}.")

D. Modify do_take(), do_examine() and do_drop()#

  1. [ ] Replace the existing value for place with a call to get_place().

  2. [ ] Remove the line assigning place_name since that is taken care of in get_place()

do_take()
283def do_take(args):
284    """Pick up an item and add it to inventory."""
285    debug(f"Trying to take: {args}")
286
287    # make sure the player typed an item
288    if not args:
289        error("What do you want to take?")
290        return
291
292    # get the item name from arguments
293    # and make it lowercase
294    name = args[0].lower()
295
296    # look up where the player is now
297    place = get_place()
298
299    # make sure the item is in this place
300    if name not in place.get("items", []):
301        error(f"Sorry, I don't see a {name!r} here.")
302        return
303
304    # get the item information
305    item = ITEMS.get(name)
306
307    # make sure the item is in the ITEMS dictionary
308    if not item:
309        abort(f"Woops! The information about {name!r} seems to be missing.")
310
311    if not item.get("can_take"):
312        error(f"You try to pick up {name!r}, but you find you aren't able to lift it.")
313        return
314
315    # add to inventory
316    PLAYER["inventory"].setdefault(name, 0)
317    PLAYER["inventory"][name] += 1
318
319    # remove from place
320    place["items"].remove(name)
321
322    wrap(f"You pick up {item['name']} and put it in your pack.")
do_examine()
212def do_examine(args):
213    """Look at an item in the current place."""
214
215    debug(f"Trying to examine: {args}")
216
217    # make sure the player said what they want to examine
218    if not args:
219        error("What do you want to examine?")
220        return
221
222    # look up where the player is now
223    place = get_place()
224
225    # get the item entered by the user and make it lowercase
226    name = args[0].lower()
227
228    # make sure the item is in this place or in the players inventory
229    if not (name in place.get("items", []) or name in PLAYER["inventory"]):
230        error(f"Sorry, I don't know what this is: {name!r}.")
231        return
232
233    # make sure the item is in the ITEMS dictionary
234    if name not in ITEMS:
235        abort(f"Woops! The information about {name} seems to be missing.")
236
237    # get the item dictionary
238    item = ITEMS[name]
239
240    # print the item information
241    header(item["name"].title())
242    wrap(item["description"])
do_drop()
341def do_drop(args):
342    """Remove an item from inventory"""
343
344    debug(f"Trying to drop: {args}.")
345
346    # make sure the player typed an item
347    if not args:
348        error("What do you want to drop?")
349        return
350
351    # get the item name from arguments
352    # and make it lowercase
353    name = args[0].lower()
354
355    # make sure the item is in inventory
356    if name not in PLAYER["inventory"] or not PLAYER["inventory"][name]:
357        error(f"You don't have any {name!r}.")
358        return
359
360    # remove from inventory
361    PLAYER["inventory"][name] -= 1
362    if not PLAYER["inventory"][name]:
363        PLAYER["inventory"].pop(name)
364
365    # look up where the player is now
366    place = get_place()
367
368    # add to place items
369    place.setdefault("items", [])
370    place["items"].append(name)
371
372    # print a message
373    wrap(f"You set down the {name}.")

Part 9.3: Add get_item()#

In this section we’ll be adding a get_item() function which will be very similar to get_place() but for items instead of places.

A. Define get_item()#

The get_item() function will be almost exactly like get_place(). It will also take one argument, the key to get from the ITEMS dictionary, but it will not be optional. We’ll call abort() if the key is not in the ITEMS dictionary and finally return the item otherwise.

  1. [ ] define a get_item() function that takes one argument key

  2. [ ] use the .get() method on the ITEMS dictionary to get the value associated from the key key and assign it to the variable item

  3. [ ] If item is falsy,

    • [ ] Use the abort() function to print an error message like:

      "Woops! The information about the item name seems to be missing."

  4. [ ] return item

Code
139def get_item(key):
140    """Return item information from ITEMS dictionary associated with key. If no
141       item is found, print an error message and return None."""
142    item = ITEMS.get(key)
143
144    if not item:
145        abort(f"Woops! The information about {key!r} seems to be missing.")
146
147    return item

B. Modify do_look() and do_inventory(): call get_item()#

Throughout the rest of the program, you’ll replace anywhere that you get an item from the ITEMS dictionary using subscription or the .get() method with a call to get_item().

  1. [ ] Call get_item() with the argument name or key to get the value for item. This will replace the existing ITEMS.get(key) or ITEMS[key].

do_look()
169def do_look():
170    """Look at the current place"""
171
172    debug("Trying to look around.")
173
174    # look up where the player is now
175    place = get_place()
176
177    # print information about the current place
178    header(f"{place['name']}")
179    wrap(place["description"])
180
181    # get the items in the room
182    items = place.get("items", [])
183
184    if items:
185
186        # for each of the place items
187        # get the info from the ITEMS dictionary
188        # and make a list of item names
189        names = []
190        for key in items:
191            item = get_item(key)
192            names.append(item["name"])
193
194        # remove the last name from the list
195        last = names.pop()
196
197        # construct a sentence that looks like one of:
198        #   x, x and y
199        #   x and y
200        #   y
201        text = ", ".join(names)
202        if text:
203            text += " and "
204        text += last
205
206        # print the list of items.
207        print()
208        wrap(f"You see {text}.\n")
209
210    # add a blank line
211    print()
212
213    # print what is in each direction from here
214    for direction in ("north", "east", "south", "west"):
215        name = place.get(direction)
216        if not name:
217            continue
218
219        destination = get_place(name)
220        write(f"To the {direction} is {destination['name']}.")
do_inventory()
326def do_inventory():
327    """Show the players inventory"""
328
329    debug("Trying to show inventory.")
330
331    header("Inventory")
332
333    if not PLAYER["inventory"]:
334        write("Empty.")
335        return
336
337    for name, qty in PLAYER["inventory"].items():
338        item = get_item(name)
339        write(f"(x{qty:>2})  {item['name']}")
340
341    print()

C. Modify do_examine() and do_take(): call get_item()#

In these functions we’ll do a similar replacement as above. Additionally, we’ll remove error handling that is done in get_item().

  1. [ ] Call get_item() with the argument name or key to get the value for item. This will replace the existing ITEMS.get(key) or ITEMS[key].

  2. [ ] Remove the lines that calls abort() if item is falsy or if name is not in ITEMS.

do_examine()
222def do_examine(args):
223    """Look at an item in the current place."""
224
225    debug(f"Trying to examine: {args}")
226
227    # make sure the player said what they want to examine
228    if not args:
229        error("What do you want to examine?")
230        return
231
232    # look up where the player is now
233    place = get_place()
234
235    # get the item entered by the user and make it lowercase
236    name = args[0].lower()
237
238    # make sure the item is in this place or in the players inventory
239    if not (name in place.get("items", []) or name in PLAYER["inventory"]):
240        error(f"Sorry, I don't know what this is: {name!r}.")
241        return
242
243    # get the item dictionary
244    item = get_item(name)
245
246    # print the item information
247    header(item["name"].title())
248    wrap(item["description"])
do_take()
289def do_take(args):
290    """Pick up an item and add it to inventory."""
291    debug(f"Trying to take: {args}")
292
293    # make sure the player typed an item
294    if not args:
295        error("What do you want to take?")
296        return
297
298    # get the item name from arguments
299    # and make it lowercase
300    name = args[0].lower()
301
302    # look up where the player is now
303    place = get_place()
304
305    # make sure the item is in this place
306    if name not in place.get("items", []):
307        error(f"Sorry, I don't see a {name!r} here.")
308        return
309
310    # get the item information
311    item = get_item(name)
312
313    if not item.get("can_take"):
314        error(f"You try to pick up {name!r}, but you find you aren't able to lift it.")
315        return
316
317    # add to inventory
318    PLAYER["inventory"].setdefault(name, 0)
319    PLAYER["inventory"][name] += 1
320
321    # remove from place
322    place["items"].remove(name)
323
324    wrap(f"You pick up {item['name']} and put it in your pack.")

Part 9.4: Validation functions#

In this section we’ll be several functions return True or False so that they can be used for things we commonly need to check. Specifically the functions player_has(), place_has(), and is_for_sale().

A. Define player_has()#

The player_has() function will return True if the player has a particular item in inventory.

  1. [ ] define a player_has() function that takes one argument key, and an optional argument qty with a default value of 1

  2. [ ] Check if the key is in the players inventory (stored in the PLAYER dict with the "inventory" key), and if so, if the value is greater than or equal to qty.

    • [ ] If so, return True

    • [ ] If not, return False

Code
151def player_has(key, qty=1):
152    """Return True if the player has at least qty item(s) associated with key in
153       their inventory."""
154    return key in PLAYER["inventory"] and PLAYER["inventory"][key] >= qty

B. Modify do_drop(): call player_has()#

Now in our if statements where we check the same thing we can replace it with a call to player_has().

  1. [ ] Find the if statement where we check to see if the item is in not inventory.

  2. [ ] Replace the part of the condition that checks with a call to player_has() and pass the argument name(). (Be sure to keep the not.)

Code
359def do_drop(args):
360    """Remove an item from inventory"""
361
362    debug(f"Trying to drop: {args}.")
363
364    # make sure the player typed an item
365    if not args:
366        error("What do you want to drop?")
367        return
368
369    # get the item name from arguments
370    # and make it lowercase
371    name = args[0].lower()
372
373    # make sure the item is in inventory
374    if not player_has(name):
375        error(f"You don't have any {name!r}.")
376        return
377
378    # remove from player inventory
379    PLAYER["inventory"][name] -= 1
380    if not PLAYER["inventory"][name]:
381        PLAYER["inventory"].pop(name)
382
383    # look up where the player is now
384    place = get_place()
385
386    # add to place items
387    place.setdefault("items", [])
388    place["items"].append(name)
389
390    # print a message
391    wrap(f"You set down the {name}.")

C. Call player_has() from do_examine()#

  1. [ ] Find the if statement where we check to see if the item is not in either the current place or the inventory.

  2. [ ] Replace the part of the condition that checks with a call to player_has() and pass the argument name(). (Be sure to keep the not, as well as the part that checks if the item is in the current place.)

Code
238def do_examine(args):
239    """Look at an item in the current place."""
240
241    debug(f"Trying to examine: {args}")
242
243    # make sure the player said what they want to examine
244    if not args:
245        error("What do you want to examine?")
246        return
247
248    # look up where the player is now
249    place = get_place()
250
251    # get the item entered by the user and make it lowercase
252    name = args[0].lower()
253
254    # make sure the item is in this place or in the players inventory
255    if not (place_has(name) or player_has(name)):
256        error(f"Sorry, I don't know what this is: {name!r}.")
257        return
258
259    # get the item dictionary
260    item = get_item(name)
261
262    # print the item information
263    header(item["name"].title())
264    wrap(item["description"])

D. Define place_has()#

The place_has() function will return True if the place has a particular item.

  1. [ ] define a place_has() function that takes one argument item.

  2. [ ] In the function get the current place by calling get_place() assign it to the variable place.

  3. [ ] Check if the item is in the list of items in the current place by using the .get() method on place with the key "items".

    • [ ] If so, return True

    • [ ] If not, return False

Code
156def place_has(item):
157    """Return True if current place has a particular item."""
158    place = get_place()
159    return item in place.get("items", [])

E. Call place_has() from do_take()#

Now in our if statements where we check the same thing we can replace it with a call to place_has().

  1. [ ] Find the if statement where we check to see if the item is not in the current place.

  2. [ ] Replace the part of the condition that checks with a call to place_has() and pass the argument name(). (Be sure to keep the not.)

Code
305def do_take(args):
306    """Pick up an item and add it to inventory."""
307    debug(f"Trying to take: {args}")
308
309    # make sure the player typed an item
310    if not args:
311        error("What do you want to take?")
312        return
313
314    # get the item name from arguments
315    # and make it lowercase
316    name = args[0].lower()
317
318    # look up where the player is now
319    place = get_place()
320
321    # make sure the item is in this place
322    if not place_has(name):
323        error(f"Sorry, I don't see a {name!r} here.")
324        return
325
326    # get the item information
327    item = get_item(name)
328
329    if not item.get("can_take"):
330        error(f"You try to pick up {name!r}, but you find you aren't able to lift it.")
331        return
332
333    # add to inventory
334    PLAYER["inventory"].setdefault(name, 0)
335    PLAYER["inventory"][name] += 1
336
337    # remove from place
338    place["items"].remove(name)
339
340    wrap(f"You pick up {item['name']} and put it in your pack.")

F. Call place_has() from do_examine()#

Now in our if statements where we check the same thing we can replace it with a call to place_has().

  1. [ ] Find the if statement where we check to see if the item is not in the current place.

  2. [ ] Replace the part of the condition that checks with a call to place_has() and pass the argument name. (Be sure to keep the not as well as the part that checks if the player has the item.)

Code
238def do_examine(args):
239    """Look at an item in the current place."""
240
241    debug(f"Trying to examine: {args}")
242
243    # make sure the player said what they want to examine
244    if not args:
245        error("What do you want to examine?")
246        return
247
248    # look up where the player is now
249    place = get_place()
250
251    # get the item entered by the user and make it lowercase
252    name = args[0].lower()
253
254    # make sure the item is in this place or in the players inventory
255    if not (place_has(name) or player_has(name)):
256        error(f"Sorry, I don't know what this is: {name!r}.")
257        return
258
259    # get the item dictionary
260    item = get_item(name)
261
262    # print the item information
263    header(item["name"].title())
264    wrap(item["description"])

G. Define is_for_sale()#

The is_for_sale() function will return True if an item is for sale.

  1. [ ] Define a is_for_sale() function that takes one argument, item.

  2. [ ] Check if the "price" key is in the item dictionary.

    • [ ] If so, return True

    • [ ] If not, return False

Code
161def is_for_sale(item):
162    """Return True if item is for sale (has a price)."""
163    return "price" in item

H. Call is_for_sale() from do_shop()#

Now in our if statements where we check the same thing we can replace it with a call to is_for_sale().

  1. [ ] Find the if statement where we check to see if the "price" key is in an item dictionary

  2. [ ] Replace the part of the condition that checks with a call to is_for_sale() and pass the argument item. (Be sure to keep the not.)

Code
167def do_shop():
168    """List the items for sale."""
169
170    header("Items for sale.")
171
172    for item in ITEMS.values():
173        if not is_for_sale(item):
174            continue
175
176        write(f'{item["name"]:<13}  {item["description"]}')
177
178    print()

Part 9.5: Add inventory_change()#

In this section we’ll be adding an inventory_change() function that will add or remove items from the players inventory.

A. Define inventory_change()#

The inventory_change() function will handle either adding items to a player’s inventory or removing from it. We’ll use an optional argument quantity for the number to add. (If the number is negative, then it the quantity will be subtracted.)

In this function we’ll use the .setdefault() method on the inventory dictionary. This is kind of like the inverse of the .get() method–if the key is not currently in the dictionary, it will set it default value. Otherwise, nothing will happen.2

Once we are done changing the quantity in inventory, we’ll remove the entire item from dictionary if there is 0 (or less) of them left. This way items with a zero quantity won’t show up in do_inventory().

  1. [ ] Define a inventory_change() function that takes one argument key, and an optional argument quantity with a default value of 1

  2. [ ] Call the .setdefault() method on the players inventory (accessed in the PLAYER dict with the "inventory" key) with the arguments key and 0 for the default.

  3. [ ] Add quantity to the players inventory (accessed in the PLAYER dict with the "inventory" key) for the key key.

    Hint: Use the += operator.

  4. [ ] Check if the value associated with key in the players inventory is falsy, or if it is less than or equal to zero.

    • [ ] If so, remove that key from the player’s inventory by calling the .pop() method on the inventory dictionary with the key argument.

Code
150def inventory_change(key, quantity=1):
151    """Add item to player inventory."""
152    PLAYER["inventory"].setdefault(key, 0)
153    PLAYER["inventory"][key] += quantity
154
155    # remove from inventory dictionary if quantity is zero
156    if not PLAYER["inventory"][key]:
157        PLAYER["inventory"].pop(key)

B. Call inventory_change() in do_take()#

Now we can call inventory_change() anytime we want to add or remove something from inventory. Let’s start in the do_take() function. Since we currently can only have one of something in a room at a time, we won’t pass the quantity argument, so it will default to 1.

  1. [ ] Find where you add the item to the player’s inventory. Replace those lines with a call to inventory_change() and pass the name argument.

Code
315def do_take(args):
316    """Pick up an item and add it to inventory."""
317    debug(f"Trying to take: {args}")
318
319    # make sure the player typed an item
320    if not args:
321        error("What do you want to take?")
322        return
323
324    # get the item name from arguments
325    # and make it lowercase
326    name = args[0].lower()
327
328    # look up where the player is now
329    place = get_place()
330
331    # make sure the item is in this place
332    if not place_has(name):
333        error(f"Sorry, I don't see a {name!r} here.")
334        return
335
336    # get the item information
337    item = get_item(name)
338
339    if not item.get("can_take"):
340        error(f"You try to pick up {name!r}, but you find you aren't able to lift it.")
341        return
342
343    # add to inventory
344    inventory_change(name)
345
346    # remove from place
347    place["items"].remove(name)
348
349    wrap(f"You pick up {item['name']} and put it in your pack.")

C. Call inventory_change() in do_drop()#

Next, we’ll call it from do_drop().

Here we’ll be subtracting from inventory by passing in a negative value for quantity by putting a - in front of the value. Then when the negative number is added to the current inventory value in inventory_change(), it will be the same as if we had subtracted a positive number.

We run into a little issue, since we can have more than one of something in the player’s inventory, but only one of something in a place. To account for this, we’ll zero out that item in inventory and only add one item to place. (Someday we may want to make the place "items" a dictionary instead of a list so we can have more than one of a thing in a particular place, but this will have to do for now.)

  1. [ ] Find where you remove the item from the player’s inventory. Right above that, get the amount the player has in their inventory (stored in the PLAYER dict with the "inventory" key) using the name key and assign it to the qty variable.

  2. [ ] Replace the lines where you add to the inventory with a call to inventory_change() with the arguments name and -qty.

Code
368def do_drop(args):
369    """Remove an item from inventory"""
370
371    debug(f"Trying to drop: {args}.")
372
373    # make sure the player typed an item
374    if not args:
375        error("What do you want to drop?")
376        return
377
378    # get the item name from arguments
379    # and make it lowercase
380    name = args[0].lower()
381
382    # make sure the item is in inventory
383    if not player_has(name):
384        error(f"You don't have any {name!r}.")
385        return
386
387    # get the amount currently in inventory
388    qty = PLAYER["inventory"][name]
389
390    # remove from player inventory
391    inventory_change(name, -qty)
392
393    # look up where the player is now
394    place = get_place()
395
396    # add to place items
397    place.setdefault("items", [])
398    place["items"].append(name)
399
400    # print a message
401    wrap(f"You set down the {name}.")

Part 9.6: Add place_add()#

In this section we’ll be adding an place_add() function that will add an item to the current place.

A. Define place_add()#

The place_add() function will take care of looking up the current place, making sure that the places "items" dictionary is set to an empty list if it’s missing, making sure that it’s not already in the place, and finally adding the item key to the place list.

  1. [ ] Define a place_add() function that takes one argument key.

  2. [ ] Get the current place by calling get_place() and assign it to the variable place.

  3. [ ] Call .setdefault() on place with the arguments "items" and an empty list.

  4. [ ] Check if key is in the place["items"] list

    • [ ] If not, append key to the place["items"] dict

Code
159def place_add(key):
160    """Add an item to the current place."""
161    # get the current place
162    place = get_place()
163
164    # add the item key to the place items list
165    place.setdefault("items", [])
166    if key not in place["items"]:
167        place["items"].append(key)

B. Call place_add() in do_drop()#

Now we can call place_add() anytime we want to add something to a place. Right now, this only happens in the do_drop() function.

  1. [ ] Find where you add the item to the place. Replace those lines with a call to place_add() and pass the name argument.

  2. [ ] You can also remove the line where you get the current place using the get_place() function.

Code
378def do_drop(args):
379    """Remove an item from inventory"""
380
381    debug(f"Trying to drop: {args}.")
382
383    # make sure the player typed an item
384    if not args:
385        error("What do you want to drop?")
386        return
387
388    # get the item name from arguments
389    # and make it lowercase
390    name = args[0].lower()
391
392    # make sure the item is in inventory
393    if not player_has(name):
394        error(f"You don't have any {name!r}.")
395        return
396
397    # get the amount currently in inventory
398    qty = PLAYER["inventory"][name]
399
400    # remove from player inventory
401    inventory_change(name, -qty)
402
403    # add to place items
404    place_add(name)
405
406    # print a message
407    wrap(f"You set down the {name}.")

Part 9.7: Add place_remove()#

In this section we’ll be adding an place_remove() function that will remove an item from the current place.

A. Define place_remove()#

The place_remove() function will take care of looking up the current place, making sure that the item is in the place, and finally removing the item key from the place list.

  1. [ ] Define a place_remove() function that takes one argument key.

  2. [ ] Get the current place by calling get_place() and assign it to the variable place.

  3. [ ] Check if key is in the current place by calling .get() on place and passing the arguments key and an empty list for the default value.

    • [ ] If so, call .remove() on place["items"] with the key argument

Code
169def place_remove(key):
170    """Remove an item from the current place."""
171    # get the current place
172    place = get_place()
173
174    # remove from place
175    if key in place.get("items", []):
176        place["items"].remove(key)

B. Call place_remove() in do_take()#

Now we can call place_remove() anytime we want to remove something from a place. Right now, this only happens in the do_take() function.

  1. [ ] Find where you remove the item from the place. Replace those lines with a call to place_remove() and pass the name argument.

  2. [ ] You can also remove the line where you get the current place using the get_place() function.

Code
334def do_take(args):
335    """Pick up an item and add it to inventory."""
336    debug(f"Trying to take: {args}")
337
338    # make sure the player typed an item
339    if not args:
340        error("What do you want to take?")
341        return
342
343    # get the item name from arguments
344    # and make it lowercase
345    name = args[0].lower()
346
347    # make sure the item is in this place
348    if not place_has(name):
349        error(f"Sorry, I don't see a {name!r} here.")
350        return
351
352    # get the item information
353    item = get_item(name)
354
355    if not item.get("can_take"):
356        error(f"You try to pick up {name!r}, but you find you aren't able to lift it.")
357        return
358
359    # add to inventory
360    inventory_change(name)
361
362    # remove from place
363    place_remove(name)
364
365    wrap(f"You pick up {item['name']} and put it in your pack.")

1

https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

2

See help(dict.setdefault) in a python/iPython shell for more information.