Part 10: Buy things#

In this section we’ll add the buy command, add the market, make sure that the buy and shop commands only work in the market, and make add information to the buy shop and examine commands.

Part 10.1: Add market#

First we’ll need to add the market to our PLACES dictionary so we can navigate to and from there.

Demo

A. Add market to PLACES#

  1. [ ] Add a "market" dictionary to your PLACES dictionary.

    • [ ] Be sure to add the relevant directions. For example, since I have it just north of "town-square" I have "south": "town-square". But you can put it somewhere else if it suits you.

    • [ ] Also add the "items" list with a list of keys of the items that are for sale in the market. For example, I have "book" and "dagger" in mine.

  2. [ ] Add the direction values to the other adjacent place dictionaries. For example, in my "town-square" dictionary I have "north": "market".

  3. [ ] Test this by making sure you can get to and from the market.

Code
43PLACES = {
44    "home": {
45        "key": "home",
46        "name": "Your Cottage",
47        "east": "town-square",
48        "description": "A cozy stone cottage with a desk and a neatly made bed.",
49        "items": ["desk", "book"],
50    },
51    "town-square": {
52        "key": "town-square",
53        "name": "The Town Square",
54        "west": "home",
55        "north": "market",
56        "description": (
57            "A large open space surrounded by buildings with a burbling "
58            "fountain in the center."
59        ),
60    },
61    "market": {
62        "key": "market",
63        "name": "The Market",
64        "south": "town-square",
65        "items": ["elixir", "dagger"],
66        "description": (
67            "A tidy store with shelves full of goods to buy. A wooden hand "
68            "painted menu hangs on the wall."
69        ),
70    },
71}
72

B. In do_shop() get items from the market#

Now that we have a legit market, we can get our items from the market rather than going through all items.

  1. [ ] At the beginning of the do_shop() function, get the market dictionary by calling get_place() and assign it to the variable place.

  2. [ ] Change your for loop, instead of iterating over ITEMS.values(), use the .get() method on place with the arguments "items" and an empty list, and iterate over that instead. Also rename the variable from item to key.

  3. [ ] Inside the for loop at the beginning, call get_item() with the argument key and assign it to the variable item.

Code
224def do_shop():
225    """List the items for sale."""
226
227    place = get_place()
228
229    header("Items for sale.")
230
231    for key in place.get("items", []):
232        item = get_item(key)
233        if not is_for_sale(item):
234            continue
235
236        write(f'{item["name"]:<13}  {item["description"]}')
237
238    print()

Part 10.2: Add place_can()#

Some commands can only happen when you are in a particular place. The way we initially wrote the do_shop() function, you can shop from anywhere. Now we’re going to store some extra information on place dictionaries to let us know if the action is restricted to certain places.

Similar to place "items", we’ll store this information as a list of strings, this time with the key "can".

Demo

A: In PLACES add "can" list to market#

In the next section we’ll write a function to use that information.

  1. [ ] In your market place dictionary, add the key "can"; and for the value a list with one item, "shop".

Code
61    "market": {
62        "key": "market",
63        "name": "The Market",
64        "south": "town-square",
65        "items": ["elixir", "dagger"],
66        "can": ["shop"],
67        "description": (
68            "A tidy store with shelves full of goods to buy. A wooden hand "
69            "painted menu hangs on the wall."
70        ),
71    },

B: Define place_can()#

The place_can() function will let us know if a place supports a particular action. This function will be very similar to the place_has() function, but for actions instead of items.

  1. [ ] Add the place_can() function that takes one argument, action.

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

  3. [ ] Check if action is not in the list of place items by calling .get() on place with the key "can" and an empty list for the default argument.

    • [ ] If so, return True

    • [ ] Otherwise, return False

Code
219def place_can(action):
220    """Return True if the current place supports a particular action."""
221    place = get_place()
222    return action in place.get("can", [])

C: Call place_can() from do_shop()#

  1. [ ] In do_shop() at the very beginning of the function check if shopping is supported in the current place by calling place_can() with the argument "shop".

    • [ ] If not, print an error message like Sorry, you can't action here. then return

Code
230def do_shop():
231    """List the items for sale."""
232
233    if not place_can("shop"):
234        error(f"Sorry, you can't shop here.")
235        return
236
237    place = get_place()
238
239    header("Items for sale.")
240
241    for key in place.get("items", []):
242        item = get_item(key)
243        if not is_for_sale(item):
244            continue
245
246        write(f'{item["name"]:<13}  {item["description"]}')
247
248    print()

Part 10.3: Add buy command#

Now we’ll add the buy command.

Demo

A. Add game info#

First we’ll need to give the player some gems, add buy to the market "can" list, and add gems to the items list.

  1. [ ] For now, let’s give the player some free gems so we can test out buying things. Add a "gems" key to the PLAYER inventory dictionary with a value of 50 or so.

  2. [ ] In the PLACES dictionary, add a "buy" to the "can" list to the market dictionary.

  3. [ ] In ITEMS add a "gems" item.

PLAYER
37PLAYER = {
38    "place": "home",
39    "inventory": {"gems": 50},
40}
41
PLACES
42PLACES = {
43    "home": {
44        "key": "home",
45        "name": "Your Cottage",
46        "east": "town-square",
47        "description": "A cozy stone cottage with a desk and a neatly made bed.",
48        "items": ["desk", "book"],
49    },
50    "town-square": {
51        "key": "town-square",
52        "name": "The Town Square",
53        "west": "home",
54        "north": "market",
55        "description": (
56            "A large open space surrounded by buildings with a burbling "
57            "fountain in the center."
58        ),
59    },
60    "market": {
61        "key": "market",
62        "name": "The Market",
63        "south": "town-square",
64        "items": ["elixir", "dagger"],
65        "can": ["shop", "buy"],
66        "description": (
67            "A tidy store with shelves full of goods to buy. A wooden hand "
68            "painted menu hangs on the wall."
69        ),
70    },
71}
72
ITEMS
 73ITEMS = {
 74    "elixir": {
 75        "key": "elixir",
 76        "name": "healing elixir",
 77        "description": "a magical elixir that will heal what ails ya",
 78        "price": -10,
 79    },
 80    "dagger": {
 81        "key": "dagger",
 82        "name": "a dagger",
 83        "description": "a 14 inch dagger with a double-edged blade",
 84        "price": -25,
 85    },
 86    "desk": {
 87        "key": "desk",
 88        "name": "a desk",
 89        "description": (
 90            "A wooden desk with a large leather-bound book open on "
 91            "its surface."
 92        ),
 93    },
 94    "book": {
 95        "key": "book",
 96        "can_take": True,
 97        "name": "a book",
 98        "description": (
 99            "A hefty leather-bound tome open to an interesting passage."
100        ),
101    },
102    "gems": {
103        "key": "gems",
104        "name": "gems",
105        "description": (
106            "A pile of sparkling gems."
107        ),
108    },
109}
110

B: Define a do_buy() function#

Here we’ll define the function that is called when the player types "buy".

  1. [ ] Define a do_buy() function that takes one argument, args

  2. [ ] In it, use the debug() function to print something like Trying to buy args.

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")

C: In main()#

  1. [ ] Add an elif that checks if command is equal to buy

    • [ ] If so, call do_buy() and pass args

Code
510def main():
511    header("Welcome!")
512
513    while True:
514        debug(f"You are at: {PLAYER['place']}")
515
516        reply = input(fg.cyan("> ")).strip()
517        args = reply.split()
518
519        if not args:
520            continue
521
522        command = args.pop(0)
523        debug(f"Command: {command!r}, args: {args!r}")
524
525        if command in ("q", "quit", "exit"):
526            do_quit()
527
528        elif command in ("shop"):
529            do_shop()
530
531        elif command in ("g", "go"):
532            do_go(args)
533
534        elif command in ("x", "exam", "examine"):
535            do_examine(args)
536
537        elif command in ("l", "look"):
538            do_look()
539
540        elif command in ("t", "take", "grab"):
541            do_take(args)
542
543        elif command in ("i", "inventory"):
544            do_inventory()
545
546        elif command == "drop":
547            do_drop(args)
548
549        elif command == "buy":
550            do_buy(args)
551
552        else:
553            error("No such command.")
554            continue
555
556        # print a blank line no matter what
557        print()

D: In do_buy(), Make sure the place supports buying#

  1. [ ] Check if you can buy things in the current place buy calling place_can() with the argument "buy".

    • [ ] If not, print a message like Sorry, you can't buy things here. then return

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return

E: Still in do_buy(), make sure the player typed in something to buy#

  1. [ ] Check if args is falsy

    • [ ] If so, print a message with the error() function like What do you want to buy? then return

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return
471
472    # make sure the player typed an item
473    if not args:
474        error("What do you want to buy?")
475        return
476

F: Still in do_buy(), make sure the item is in this place#

  1. [ ] assign the first item of the args list to the variable name and make it lowercase

  2. [ ] check if the item is in this place by calling place_has() with the argument name

    • [ ] if not, print an error message "Sorry, I don't see a {name} here." then return

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return
471
472    # make sure the player typed an item
473    if not args:
474        error("What do you want to buy?")
475        return
476
477    # get the item name from arguments
478    # and make it lowercase
479    name = args[0].lower()
480
481    # make sure the item is in this place
482    if not place_has(name):
483        error(f"Sorry, I don't see a {name!r} here.")
484        return
485

G. Still in do_buy(), make sure the item is for sale#

  1. [ ] Get the item dictionary by calling get_item() with the argument name and assign it to the variable item.

  2. [ ] Check if the item is for sale by calling is_for_sale() with the argument item

    • [ ] If not print an error message like Sorry, name is not for sale then return

  3. [ ] To test this, add another item that is not for sale to the market, or temporarily remove the "price" from one of the items in your market.

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return
471
472    # make sure the player typed an item
473    if not args:
474        error("What do you want to buy?")
475        return
476
477    # get the item name from arguments
478    # and make it lowercase
479    name = args[0].lower()
480
481    # make sure the item is in this place
482    if not place_has(name):
483        error(f"Sorry, I don't see a {name!r} here.")
484        return
485
486    # get the item information
487    item = get_item(name)
488
489    if not is_for_sale(item):
490        error(f"Sorry, {item['name']} is not for sale.")
491        return
492

H. Still in do_buy(), make sure the player can afford the item#

  1. [ ] Get the price from the item dictionary, and make it positive (if necessary) by calling abs(), then assign it to the variable price

  2. [ ] Check if the player has enough gems by calling player_has() with the arguments "gems" and price. If not:

    • [ ] Get the number of gems the player currently has from the PLAYER inventory dictionary by calling the .get() method with the arguments "gems" and 0 for the default value. Assign it to the variable gems.

    • [ ] Print an error message like Sorry, you can't afford name because it costs price and you only have gems.

    • [ ] return

  3. [ ] To test this, temporarily change the price of one of your items to be more than the amount of gems you have.

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return
471
472    # make sure the player typed an item
473    if not args:
474        error("What do you want to buy?")
475        return
476
477    # get the item name from arguments
478    # and make it lowercase
479    name = args[0].lower()
480
481    # make sure the item is in this place
482    if not place_has(name):
483        error(f"Sorry, I don't see a {name!r} here.")
484        return
485
486    # get the item information
487    item = get_item(name)
488
489    if not is_for_sale(item):
490        error(f"Sorry, {item['name']} is not for sale.")
491        return
492
493    price = abs(item["price"])
494    if not player_has("gems", price):
495        gems = PLAYER["inventory"].get("gems", 0)
496        error(f"Sorry, you can't afford {item['name']} because it costs {price} and you only have {gems}.")
497        return
498

I. In do_buy(), buy the item#

  1. [ ] Remove gems from inventory by calling inventory_change() with the values "gems" and negative price.

  2. [ ] Add the item to inventory by calling inventory_change() with the value name

  3. [ ] Remove the item from the current place by calling place_remove() with the argument name

  4. [ ] Print a message like "You bought name."

Code
463def do_buy(args):
464    """Purchase an item."""
465
466    debug(f"Trying to buy: {args}.")
467
468    if not place_can("buy"):
469        error("Sorry, you can't buy things here.")
470        return
471
472    # make sure the player typed an item
473    if not args:
474        error("What do you want to buy?")
475        return
476
477    # get the item name from arguments
478    # and make it lowercase
479    name = args[0].lower()
480
481    # make sure the item is in this place
482    if not place_has(name):
483        error(f"Sorry, I don't see a {name!r} here.")
484        return
485
486    # get the item information
487    item = get_item(name)
488
489    if not is_for_sale(item):
490        error(f"Sorry, {item['name']} is not for sale.")
491        return
492
493    price = abs(item["price"])
494    if not player_has("gems", price):
495        gems = PLAYER["inventory"].get("gems", 0)
496        error(f"Sorry, you can't afford {item['name']} because it costs {price} and you only have {gems}.")
497        return
498
499    # remove gems from inventory
500    inventory_change("gems", -price)
501
502    # add item to inventory
503    inventory_change(name)
504
505    # remove item from place
506    place_remove(name)
507
508    wrap(f"You bought {item['name']}.")

Part 10.4: Clean up the shop#

In this section we’ll make a number of small changes to improve the shop and examine commands.

Demo

A: Show price in do_shop()#

We should add the price to the information we print out about each item. This is also a good chance to make this look prettier.

  1. [ ] Print the item "price" along with the name and description. If the number is negative, call abs() to make it a positive number.

  2. [ ] Use string formatting to align the information into columns.

Code
235def do_shop():
236    """List the items for sale."""
237
238    if not place_can("shop"):
239        error(f"Sorry, you can't shop here.")
240        return
241
242    place = get_place()
243
244    header("Items for sale.")
245
246    for key in place.get("items", []):
247        item = get_item(key)
248        if not is_for_sale(item):
249            continue
250
251        write(f'{item["name"]:<13}  {item["description"]:<45}  {abs(item["price"]):>2} gems')
252
253    print()

B. Handle long descriptions#

If your item descriptions are too long for a single line, you can do either one of the following.

I. Truncate the description#

The simplest way to handle too-long descriptions is to truncate them so that they are all limited to a specific width. There are a few ways to do this, but here we’ll use the textwrap.shorten() function.

  1. [ ] In your for loop, before you call write() to print the line, call textwrap.shorten() with two arguments: the item description, and the desired maximum width. Assign it to the variable description.

  2. [ ] In the argument to your write() function, replace item["description"] with description.

Code
233def do_shop():
234    """List the items for sale."""
235
236    if not place_can("shop"):
237        error(f"Sorry, you can't {action} here.")
238        return
239
240    place = get_place()
241
242    header("Items for sale.")
243
244    for key in place.get("items", []):
245        item = get_item(key)
246        if not is_for_sale(item):
247            continue
248
249        description = textwrap.shorten(item["description"], 30)
250        write(f'{item["name"]:<13}  {description:<30} {abs(item["price"]):>2} gems')
251
252    print()

II. Add a short "summary" to items dictionary#

Another way to deal with the problem is to separate the long "description" from a shorter "summary" in the ITEMS dictionaries. Then here in do_shop() we’ll print the "summary", and in do_examine() we’ll show the longer description.

This is fancier, but it will require coming up with more data for each item in your game.

  1. [ ] In each dictionary in ITEMS add a key "summary" with a one-line description of the item.

  2. [ ] In do_shop when printing the item information, replace item["description"] with item["summary"].

ITEMS
73    "elixr": {
74        "key": "elixr",
75        "name": "healing elixr",
76        "summary": "a healing elixer",
77        "desc": (
78            "A small corked bottle filled with a swirling green liquid. "
79            "It is said to have magical healing properties."
80        ),
81        "price": -10,
82    },
83    "dagger": {
84        "key": "dagger",
85        "name": "a dagger",
86        "summary": "a double-edged 14 inch dagger",
87        "description": (
88            "A double-edged 14 inch dagger with a crescent shaped hardwood "
89            "grip, metal cross guard, and curved studded metal pommel."
90        ),
91        "price": -25,
92    },
do_shop()
233def do_shop():
234    """List the items for sale."""
235
236    if not place_can("shop"):
237        error(f"Sorry, you can't {action} here.")
238        return
239
240    place = get_place()
241
242    header("Items for sale.")
243
244    for key in place.get("items", []):
245        item = get_item(key)
246        if not is_for_sale(item):
247            continue
248
249        write(f'{item["name"]:<13}  {items["summary"]:<30} {abs(item["price"]):>2} gems')
250
251    print()

C. In do_examine(): show price#

Let’s show the price to do_examine() if we’re in the market (or somewhere else where we can shop).

  1. [ ] In do_examine() after the header, check if:

    • the place supports shopping by calling place_can() with the argument "shop" and

    • the place has the item by calling place_has() with the argument name and

    • the item is for sale by calling is_for_sale() with the argument item

    If so:

    • [ ] print the item "price"

Code
313def do_examine(args):
314    """Look at an item in the current place."""
315
316    debug(f"Trying to examine: {args}")
317
318    # make sure the player said what they want to examine
319    if not args:
320        error("What do you want to examine?")
321        return
322
323    # look up where the player is now
324    place = get_place()
325
326    # get the item entered by the user and make it lowercase
327    name = args[0].lower()
328
329    # make sure the item is in this place or in the players inventory
330    if not (place_has(name) or player_has(name)):
331        error(f"Sorry, I don't know what this is: {name!r}.")
332        return
333
334    # get the item dictionary
335    item = get_item(name)
336
337    # print the item information
338    header(item["name"].title())
339
340    # print the price if we're in the market
341    if place_can("shop") and place_has(name) and is_for_sale(item):
342        write(f"{abs(item['price'])} gems".rjust(WIDTH - MARGIN))
343        print()
344
345    # print the quantity if the item is from inventory
346    elif player_has(name):
347        write(f"(x{PLAYER['inventory'][name]})".rjust(WIDTH - MARGIN))
348        print()
349
350    wrap(item["description"])

D. In do_examine(): show inventory quantity#

  1. [ ] Check if the player has the item in inventory by calling player_has() with the argument name. If so:

    • [ ] Print the quantity from the PLAYER inventory dictionary for name.

Code
313def do_examine(args):
314    """Look at an item in the current place."""
315
316    debug(f"Trying to examine: {args}")
317
318    # make sure the player said what they want to examine
319    if not args:
320        error("What do you want to examine?")
321        return
322
323    # look up where the player is now
324    place = get_place()
325
326    # get the item entered by the user and make it lowercase
327    name = args[0].lower()
328
329    # make sure the item is in this place or in the players inventory
330    if not (place_has(name) or player_has(name)):
331        error(f"Sorry, I don't know what this is: {name!r}.")
332        return
333
334    # get the item dictionary
335    item = get_item(name)
336
337    # print the item information
338    header(item["name"].title())
339
340    # print the price if we're in the market
341    if place_can("shop") and place_has(name) and is_for_sale(item):
342        write(f"{abs(item['price'])} gems".rjust(WIDTH - MARGIN))
343        print()
344
345    # print the quantity if the item is from inventory
346    elif player_has(name):
347        write(f"(x{PLAYER['inventory'][name]})".rjust(WIDTH - MARGIN))
348        print()
349
350    wrap(item["description"])