Part 10: Buy things
Contents
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
#
[ ]
Add a"market"
dictionary to yourPLACES
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.
[ ]
Add the direction values to the other adjacent place dictionaries. For example, in my"town-square"
dictionary I have"north": "market"
.[ ]
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.
[ ]
At the beginning of thedo_shop()
function, get the market dictionary by callingget_place()
and assign it to the variableplace
.[ ]
Change your for loop, instead of iterating overITEMS.values()
, use the.get()
method onplace
with the arguments"items"
and an empty list, and iterate over that instead. Also rename the variable fromitem
tokey
.[ ]
Inside the for loop at the beginning, callget_item()
with the argumentkey
and assign it to the variableitem
.
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.
[ ]
In yourmarket
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.
[ ]
Add theplace_can()
function that takes one argument,action
.[ ]
Get the current place by callingget_place()
and assign it to the variableplace
[ ]
Check ifaction
is not in the list of place items by calling.get()
onplace
with the key"can"
and an empty list for the default argument.[ ]
If so, returnTrue
[ ]
Otherwise, returnFalse
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()
#
[ ]
Indo_shop()
at the very beginning of the function check if shopping is supported in the current place by callingplace_can()
with the argument"shop"
.[ ]
If not, print an error message likeSorry, 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.
[ ]
For now, let’s give the player some free gems so we can test out buying things. Add a"gems"
key to thePLAYER
inventory dictionary with a value of50
or so.[ ]
In thePLACES
dictionary, add a"buy"
to the"can"
list to the market dictionary.[ ]
InITEMS
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"
.
[ ]
Define ado_buy()
function that takes one argument,args
[ ]
In it, use thedebug()
function to print something likeTrying to buy args.
Code
463def do_buy(args):
464 """Purchase an item."""
465
466 debug(f"Trying to buy: {args}.")
C: In main()
#
[ ]
Add anelif
that checks ifcommand
is equal tobuy
[ ]
If so, calldo_buy()
and passargs
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#
[ ]
Check if you can buy things in the current place buy callingplace_can()
with the argument"buy"
.[ ]
If not, print a message likeSorry, 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#
[ ]
Check ifargs
is falsy[ ]
If so, print a message with theerror()
function likeWhat 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#
[ ]
assign the first item of theargs
list to the variablename
and make it lowercase[ ]
check if the item is in this place by callingplace_has()
with the argumentname
[ ]
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#
[ ]
Get the item dictionary by callingget_item()
with the argumentname
and assign it to the variableitem
.[ ]
Check if the item is for sale by callingis_for_sale()
with the argumentitem
[ ]
If not print an error message likeSorry, name is not for sale
then return
[ ]
To test this, add another item that is not for sale to themarket
, 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#
[ ]
Get the price from the item dictionary, and make it positive (if necessary) by callingabs()
, then assign it to the variableprice
[ ]
Check if the player has enough gems by callingplayer_has()
with the arguments"gems"
andprice
. If not:[ ]
Get the number of gems the player currently has from thePLAYER
inventory dictionary by calling the.get()
method with the arguments"gems"
and0
for the default value. Assign it to the variablegems
.[ ]
Print an error message likeSorry, you can't afford name because it costs price and you only have gems.
[ ]
return
[ ]
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#
[ ]
Remove gems from inventory by callinginventory_change()
with the values"gems"
and negativeprice
.[ ]
Add the item to inventory by callinginventory_change()
with the valuename
[ ]
Remove the item from the current place by callingplace_remove()
with the argumentname
[ ]
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.
[ ]
Print theitem
"price"
along with the name and description. If the number is negative, callabs()
to make it a positive number.[ ]
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.
[ ]
In your for loop, before you callwrite()
to print the line, calltextwrap.shorten()
with two arguments: the item description, and the desired maximum width. Assign it to the variabledescription
.[ ]
In the argument to yourwrite()
function, replaceitem["description"]
withdescription
.
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.
[ ]
In each dictionary inITEMS
add a key"summary"
with a one-line description of the item.[ ]
Indo_shop
when printing the item information, replaceitem["description"]
withitem["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).
[ ]
Indo_examine()
after the header, check if:the place supports shopping by calling
place_can()
with the argument"shop"
andthe place has the item by calling
place_has()
with the argumentname
andthe item is for sale by calling
is_for_sale()
with the argumentitem
If so:
[ ]
print theitem
"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#
[ ]
Check if the player has the item in inventory by callingplayer_has()
with the argumentname
. If so:[ ]
Print the quantity from thePLAYER
inventory dictionary forname
.
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"])