Part 14: Dragons
Contents
Part 14: Dragons#
In this section we will add a cave with a three headed dragon and the command to pet them.
Part 14.1: Add command#
In this section we’ll add the pet command.
Demo
A. In test_game.py define test_do_pet()#
First we’ll write the test which we expect to fail. It will just test that when
you call do_pet() a debug message is printed.
Need help?
- [ ]Import the- do_petfunction
- [ ]Add- test_do_pet()function with one parameter- capsys
- [ ]Call- do_pet()with an empty list as an argument
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
- [ ]Write an assert statement that checks that the debug message- "Trying to pet: []"is in- output
- [ ]Run your tests. They should fail.
Code
 6from adventure import (
 7    debug,
 8    do_drop,
 9    do_read,
10    do_pet,
11    error,
12    header,
13    health_change,
14    inventory_change,
15    is_for_sale,
16    place_has,
17    player_has,
18    place_add,
19    wrap,
20    write,
21    MAX_HEALTH,
22)
441def test_do_pet(capsys):
442    # WHEN: You call do_pet()
443    do_pet([])
444    output = capsys.readouterr().out
445
446    # THEN: A debug message should be printed
447    assert "Trying to pet: []" in output
B. In adventure.py define do_pet()#
Now we’ll add the do_pet() function to our game. It should print a debug
message like Trying to pet: 
Need help?
- [ ]Add a- do_pet()function with one parameter- args
- [ ]In it, use the- debug()function to print something like- "Trying to pet args.".
- [ ]Run your tests again. They should now pass.
do_pet()
652def do_pet(args):
653    """Pet dragons"""
654
655    debug(f"Trying to pet: {args}")
C. In adventure.py modify main(): add pet#
Finally, add the code in main() so that when the player types "pet", the
do_pet() function will be called.
Need help?
- [ ]Add an elif that checks if command is- "pet".- [ ]if so, call- do_pet()and pass- args.
 
main()
673        if command in ("q", "quit", "exit"):
674            do_quit()
675
676        elif command in ("shop"):
677            do_shop()
678
679        elif command in ("g", "go"):
680            do_go(args)
681
682        elif command in ("x", "exam", "examine"):
683            do_examine(args)
684
685        elif command in ("l", "look"):
686            do_look()
687
688        elif command in ("t", "take", "grab"):
689            do_take(args)
690
691        elif command in ("i", "inventory"):
692            do_inventory()
693
694        elif command in ("r", "read"):
695            do_read(args)
696
697        elif command == "drop":
698            do_drop(args)
699
700        elif command == "buy":
701            do_buy(args)
702
703        elif command == "pet":
704            do_pet(args)
705
706        else:
707            error("No such command.")
708            continue
Part 14.2: Is petting allowed?#
In this section we’ll check to make sure petting is allowed in the current place.
Demo
A. In test_game.py define test_do_pet_cant_pet()#
In this section we’ll write a test_do_pet_cant_pet() function. It should
check that if the player tries to pet something when in a place where they
aren’t allowed (as defined by the place dictionary "can" list), they’ll see
an error message.
Need help?
1. GIVEN: The player is in a place where they can’t pet anything
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be an empty list.
2. WHEN: They try to pet something
…
- [ ]Call- do_pet()with a list containing any string
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
3. THEN: An error message should be printed
…
- [ ]assert that an error message like- "You can't do that"is in- output
4. Run your tests. They should fail.
test_do_pet_cant_pet()
450def test_do_pet_cant_pet(capsys):
451    # GIVEN: The player is in a place where they can't pet anything
452    adventure.PLAYER["place"] = "nowhere"
453    adventure.PLACES["nowhere"] = {
454        "name": "The Void",
455        "can": [],
456    }
457
458    # WHEN: They try to pet something
459    do_pet(["red", "dragon"])
460    output = capsys.readouterr().out
461
462    # THEN: An error message should be printed
463    assert "You can't do that" in output
B. In adventure.py modify do_pet(): can pet#
Now we’ll modify do_pet() function to check that if the current place is not
able to use the pet command (as defined by the place dictionary "can" list)
an error message will be printed and the function will return.
Need help?
- [ ]Use the- place_can()function to check if the place can- "pet". If not:- [ ]Print an error message like- "You can't do that here."
- [ ]return
 
do_pet()
699def do_pet(args):
700    """Pet dragons"""
701
702    debug(f"Trying to pet: {args}")
703
704    # make sure they are somewhere they can pet dragons
705    if not place_can("pet"):
706        error("You can't do that here.")
707        return
C. In adventure.py modify PLACES#
Now update the PLACES dictionary to add a cave where you can pet a dragon,
and modify your other places so that you can get to it.
Need help?
- [ ]Add a place called- cavewith the- "can"key set to a list that includes the string- "pet"
- [ ]Modify the- "east",- "west",- "north", and- "south"key(s) of your other places so that the player can get to the cave.
PLACES
 53PLACES = {
 54    "home": {
 55        "key": "home",
 56        "name": "Your Cottage",
 57        "east": "town-square",
 58        "description": "A cozy stone cottage with a desk and a neatly made bed.",
 59        "items": ["desk", "book"],
 60    },
 61    "town-square": {
 62        "key": "town-square",
 63        "name": "The Town Square",
 64        "west": "home",
 65        "east": "woods",
 66        "north": "market",
 67        "description": (
 68            "A large open space surrounded by buildings with a burbling "
 69            "fountain in the center."
 70        ),
 71    },
 72    "market": {
 73        "key": "market",
 74        "name": "The Market",
 75        "south": "town-square",
 76        "items": ["elixir", "dagger"],
 77        "can": ["shop", "buy"],
 78        "description": (
 79            "A tidy store with shelves full of goods to buy. A wooden hand "
 80            "painted menu hangs on the wall."
 81        ),
 82    },
 83    "woods": {
 84        "key": "woods",
 85        "name": "The Woods",
 86        "east": "hill",
 87        "west": "town-square",
 88        "description": (
 89            "A dirt road meanders under a canopy of autumn leaves in brilliant "
 90            "hues of gold and crimson.",
 91
 92            "You hear a stream burbling somewhere out of sight. Leaves crunch "
 93            "under your feet on the sun dappled forest floor.",
 94
 95            "You see an ancient moss-covered hollow tree, its gnarled and twisted "
 96            "branches looming over you. On the opposite side, a fallen log juts "
 97            "partway into the road.",
 98        ),
 99        "items": [],
100    },
101    "hill": {
102        "key": "hill",
103        "name": "A grassy hill",
104        "west": "woods",
105        "south": "cave",
106        "description": (
107            "A winding path leads up the slope of a grassy hill. The air is "
108            "warm here.",
109            "At the top of the hill, you see that the path continues to the "
110            "down to the south. In that direction you can make out a cave by "
111            "the shore of a lake."
112        ),
113        "items": [],
114    },
115    "cave": {
116        "key": "cave",
117        "name": "A cave",
118        "north": "hill",
119        "description": (
120            "Your footsteps echo as you step into the vast cavern.",
121            "Shafts of sunlight slice through the gloom, playing against the "
122            "landscape of glittering treasure.",
123            "Resting atop a mound of gold, a collosal dragon rests curled up snugly. "
124            "Its three enormous heads snore softly, each in turn.",
125        ),
126        "items": [],
127        "can": ["pet"],
128    },
129}
130
Part 14.3: Ensure args#
In this section we’ll make sure that the player typed what they want to pet, or print an error if they didn’t.
Demo
A. In test_game.py define test_do_pet_no_args()#
In this section we’ll write a test_do_pet_no_args() function. It should check
that if the player does not type anything after "pet", they’ll see an error
message.
Need help?
1. GIVEN: The player is in a place where they can pet things
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be a list containing the string- "pet"
2. WHEN: the player types “pet” with no arguments
…
- [ ]Call- do_pet()with an empty list
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
3. THEN: an error message should be printed
…
- [ ]assert that an error message like- "What do you want to pet"is in- output
4. Run your tests. They should fail.
test_do_pet_no_args()
466def test_do_pet_no_args(capsys):
467    # GIVEN: The player is in a place where they can pet things
468    adventure.PLAYER["place"] = "somewhere"
469    adventure.PLACES["somewhere"] = {
470        "name": "Somewhere out there",
471        "can": ["pet"],
472    }
473
474    # WHEN: the player types "pet" with no arguments
475    do_pet([])
476    output = capsys.readouterr().out
477
478    # THEN: an error message should be printed
479    assert "What do you want to pet" in output
B. In adventure.py modify do_pet(): ensure args#
Need help?
- [ ]Check if- argsis empty. If so:- [ ]Print an error message like- "What do you want to pet?"
- [ ]return
 
do_pet()
699def do_pet(args):
700    """Pet dragons"""
701
702    debug(f"Trying to pet: {args}")
703
704    # make sure they are somewhere they can pet dragons
705    if not place_can("pet"):
706        error("You can't do that here.")
707        return
708
709    # make sure the player said what they want to pet
710    if not args:
711        error("What do you want to pet?")
712        return
Part 14.4: Ensure color#
This command is a little different from previous commands, because we want the player to be able to type a few different things.
We expect the player to type something like:
pet red dragon
But we would also accept:
pet red dragon head
Or:
pet red head
Or even:
pet red
Demo
So we’ll need to make sure that the player typed something in addition to
"dragon" and "head" and that it is a valid color.
A. In test_game.py define test_do_pet_no_color()#
In this section we’ll write a test_do_pet_no_color() test. It should
check that the player typed something in addition to "dragon" and/or
"head".
Need help?
1. GIVEN: The player is in a place where they can pet things
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be a list containing the string- "pet"
2. WHEN: the player types “pet” without typing a color
…
- [ ]Call- do_pet()with a list containing the words- "dragon"and/or- "head"
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
3. THEN: an error message should be printed
…
- [ ]assert that an error message like- "What do you want to pet"is in- output
4. Run your tests. They should fail.
test_do_pet_no_color()
482def test_do_pet_no_color(capsys):
483    # GIVEN: The player is in a place where they can pet things
484    adventure.PLAYER["place"] = "somewhere"
485    adventure.PLACES["somewhere"] = {
486        "name": "Somewhere out there",
487        "can": ["pet"],
488    }
489
490    # WHEN: the player types "pet" with only the words "dragon" and/or "head"
491    do_pet(["dragon", "head"])
492    output = capsys.readouterr().out
493
494    # THEN: an error message should be printed
495    assert "What do you want to pet" in output
B. In adventure.py modify do_pet(): remove ignored args#
To support extra words like "dragon" and "head", we’re simply going to
remove them from args.
If we do this before we check to make sure that args is not empty, then
we’ll get the same error message if they type pet as when they type pet dragon.
Need help?
Do this before the line with if not args:
- [ ]Make a list of allowed words like- ["dragon", "head"]and iterate over it. For each one:- [ ]Check if the word is in- args. If so:- [ ]Remove it from- args
 
 
- [ ]Run your tests. They should pass.
do_pet()
701def do_pet(args):
702    """Pet dragons"""
703
704    debug(f"Trying to pet: {args}")
705
706    # make sure they are somewhere they can pet dragons
707    if not place_can("pet"):
708        error("You can't do that here.")
709        return
710
711    # remove the expected words from args
712    for word in ["dragon", "head"]:
713        if word in args:
714            args.remove(word)
715
716    # make sure the player said what they want to pet
717    if not args:
718        error("What do you want to pet?")
719        return
720
C. In test_game.py define test_do_pet_invalid_color()#
We’ll add a new test test_do_pet_invalid_color() to make sure the color is
valid. We’ll use a global variable adventure.COLORS to store the list of
valid colors.
Need help?
1. GIVEN: There are three colors of dragon heads
…
- [ ]Assign- adventure.COLORSto a list of colors
1. AND: The player is in a place where they can pet things
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be an empty list.
2. WHEN: They try to pet a dragon with a color that doesn’t exist
…
- [ ]Call- do_pet()with a list containing the a word that is not a color
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
3. THEN: an error message should be printed
…
- [ ]assert that an error message like- "I don't see that dragon"is in- output
4. Run your tests. They should fail.
test_do_pet_invalid_color()
498def test_do_pet_invalid_color(capsys):
499    # GIVEN: There are three colors of dragon heads
500    adventure.COLORS = ["red", "green", "blue"]
501
502    # AND: The player is in a place where they can pet dragons
503    adventure.PLAYER["place"] = "cave"
504    adventure.PLACES["cave"] = {
505        "name": "A cave",
506        "can": ["pet"],
507    }
508
509    # WHEN: They try to pet a dragon with a color that doesn't exist
510    do_pet(["purple"])
511    output = capsys.readouterr().out
512
513    # THEN: An error message should be printed
514    assert "I don't see a dragon" in output
D. In adventure.py add COLORS#
At the top of your script where your other global variables are, add a new
global variable COLORS and set it to a list with three colors in it.
COLORS
24import textwrap
25
26from console import fg, fx
27from console.progress import ProgressBar
28from console.screen import sc
29from console.utils import clear_line
30
31WIDTH = 45
32
33MARGIN = 2
34
35DEBUG = True
36
37MAX_HEALTH = 100
38
39BAR = ProgressBar(
40    total=(MAX_HEALTH + 0.1),
41    width=(WIDTH - len("Health") - len("100%")),
42    clear_left=False,
43)
44
45# ## Game World Data #########################################################
46
47COLORS = ["red", "black", "silver"]
48
E. In adventure.py modify do_pet(): ensure valid color#
We can now assume that anything left in the args list is the color. We’ll
check that it is in the COLORS list, or print an error message if it is not.
Need help?
- [ ]Assign the first item from- argsto the variable- color
- [ ]Check to make sure that- coloris in the list of- COLORS. If not:- [ ]Print an error message like:- "I don't see a dragon head that looks like that."
- [ ]return
 
do_pet()
701def do_pet(args):
702    """Pet dragons"""
703
704    debug(f"Trying to pet: {args}")
705
706    # make sure they are somewhere they can pet dragons
707    if not place_can("pet"):
708        error("You can't do that here.")
709        return
710
711    # remove the expected words from args
712    for word in ["dragon", "head"]:
713        if word in args:
714            args.remove(word)
715
716    # make sure the player said what they want to pet
717    if not args:
718        error("What do you want to pet?")
719        return
720
721    color = args[0].lower()
722
723    # make sure they typed in a real color
724    if color not in COLORS:
725        error("I don't see a dragon that looks like that.")
726        return
Part 14.5: Pick a dragon#
In this section we’ll randomly pick a dragon mood and print a debug message about it.
We’ll make a global list DRAGONS to store information about each dragon in
dictionaries. We’ll add more to this later, but for now each dictionary just
needs a single key "mood" with a string for the dragon’s mood, for example
"cheerful".
Demo
Then when the player pets one of the dragon’s heads, randomly select one of the dragon dictionaries and print a debug message that says which dragon was selected.
A. In test_game.py define test_do_pet_cheerful_dragon()#
In this section we’ll start a test for when the player pets a cheerful dragon head and simply assert that a debug message was printed.
In order to make sure we always get the cheerful dragon in the test, we’ll set
COLORS and DRAGONS to only contain one color and dragon dictionary
respectively.
Need help?
1. GIVEN: The player is in a place where they can pet a dragon
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be a list containing the string- "pet"
2. AND: There is one color of dragon heads
…
- [ ]Assign- adventure.COLORSto a list containing one color
3. AND: There is one dragon
…
- [ ]Assign- adventure.DRAGONSto a list containing one dictionary. The dictionary should have the key- "mood"and the string- "cheerful"for the value.
4. WHEN: The player pets that head
…
- [ ]Call- do_pet()with a list that contains the same color that is in- COLORS
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
5. THEN: A debug message should print
…
- [ ]assert that an debug message like- "You picked the dragon's cheerful red head."is in- output
6. Run your tests. They should fail.
test_do_pet_cheerful_dragon()
517def test_do_pet_cheerful_dragon(capsys):
518    # GIVEN: The player is in a place where they can pet a dragon
519    adventure.PLAYER["place"] = "cave"
520    adventure.PLACES["cave"] = {
521        "name": "A cave",
522        "can": ["pet"]
523    }
524
525    # AND: There is one color of dragon heads
526    adventure.COLORS = ["red"]
527
528    # AND: There is one dragon
529    adventure.DRAGONS = [
530        {"mood": "cheerful"}
531    ]
532
533    # WHEN: The player pets that head
534    do_pet(["red", "dragon"])
535    output = capsys.readouterr().out
536
537    # THEN: A debug message should print
538    assert "You picked the cheerful red dragon." in output
B. At the top of adventure.py: import random#
In order to randomly select a dragon dictionary from DRAGONS we’ll need to
import the random module.
Need help?
- [ ]import the- randommodule
Code
24import random
25import textwrap
26
27from console import fg, fx
28from console.progress import ProgressBar
29from console.screen import sc
C. At the top of adventure.py: add DRAGONS#
Add the global variable DRAGONS and assign it to a list where each item is a
dictionary containing information about each of the dragon’s heads. For now each
dictionary will only have one key "mood".
Add three dragon dictionaries for the moods "cheerful", "grumpy" and
"lonely".
Need help?
- [ ]Create global variable- DRAGONSand assign it to a list. The list should contain:- [ ]Three dictionaries. Each dictionary should contain:- [ ]The key- "mood"and string with the mood of that dragon, ie- "cheerful"
 
 
COLORS
48COLORS = ["red", "black", "silver"]
49
50DRAGONS = [
51    {"mood": "cheerful"},
52    {"mood": "grumpy"},
53    {"mood": "lonely"},
54]
55
56PLAYER = {
57    "place": "home",
58    "inventory": {"gems": 50},
59    "health": MAX_HEALTH,
60}
61
D. In adventure.py modify do_pet()#
In this section we’ll randomly select one of the dragons from DRAGONS using the
random.choice() function. We’ll add the "color" that the player selected to
that dictionary, then print a debug message with information about the dragon.
Need help?
- [ ]Call- random.choice()with the argument- DRAGONSand assign it to the variable- dragon.
- [ ]Set- dragon["color"]to- color
- [ ]Print a debug message like- "You picked the dragon's MOOD COLOR head."
do_pet()
708def do_pet(args):
709    """Pet dragons"""
710
711    debug(f"Trying to pet: {args}")
712
713    # make sure they are somewhere they can pet dragons
714    if not place_can("pet"):
715        error("You can't do that here.")
716        return
717
718    # remove the expected words from args
719    for word in ["dragon", "head"]:
720        if word in args:
721            args.remove(word)
722
723    # make sure the player said what they want to pet
724    if not args:
725        error("What do you want to pet?")
726        return
727
728    color = args[0].lower()
729
730    # make sure they typed in a real color
731    if color not in COLORS:
732        error("I don't see a dragon that looks like that.")
733        return
734
735    # get the dragon info for this color
736    dragon = random.choice(DRAGONS)
737    dragon["color"] = color
738
739    debug(f"You picked the {dragon['mood']} {dragon['color']} dragon.")
Part 14.6: Treasure#
In this section we’re going to write the code so that some of the dragons will give the player gems.
In order to do this we’ll add a "treasure" key to some of the dragon
dictionaries DRAGONS. The value will be a tuple containing the minimum and
maximum possible gems that a particular dragon might give.
Demo
Then in the do_pet() function we’ll retrieve the range of possible treasure
from the dragon dictionary, randomly pick a number in that range, then add that
amount the player gems and print a message to tell the player what happened.
A. In test_game.py modify test_do_pet_cheerful_dragon()#
In this section we’ll modify the test_do_pet_cheerful_dragon() test to add
the "treasure" key to the single dictionary in DRAGONS, and a tuple
containing two numbers (the minimum and maximum possible treasure) as the value.
Then we’ll need to check that the number if gems in the player’s inventory was increased.
Need help?
1. Modify AND: There is one dragon
…
- [ ]Add a- "treasure"key to the dragon dictionary in- DRAGONSfor- [ ]The key should be- "treasure"
- [ ]The value should a tuple with two numbers representing the minimum and maximum possible treasure. ie- (10, 20).
 
Tip
If we want to know the exact treasure amount you can use the same
number for the minimum and maximum, for example: (10, 10).
2. After WHEN add AND: The player has some gems
…
- [ ]Set the- "gems"in- PLAYERinventory to a number
3. After THEN add AND: The player should get treasure
…
- [ ]assert that the gems in- PLAYERinventory is more than it was before
4. After THEN add AND: The player should see a message about what happened
…
- [ ]assert a message like- "gave you GEMS gems"is in- output
5. Run your tests. They should fail.
test_do_pet_cheerful_dragon()
517def test_do_pet_cheerful_dragon(capsys):
518    # GIVEN: The player is in a place where they can pet a dragon
519    adventure.PLAYER["place"] = "cave"
520    adventure.PLACES["cave"] = {
521        "name": "A cave",
522        "can": ["pet"]
523    }
524
525    # AND: There is one color of dragon heads
526    adventure.COLORS = ["red"]
527
528    # AND: There is one dragon who gives you treasure
529    adventure.DRAGONS = [{
530        "mood": "cheerful",
531        "treasure": (10, 10),
532    }]
533
534    # AND: The player has some gems
535    adventure.PLAYER["inventory"] = {"gems": 10}
536
537    # WHEN: The player pets that head
538    do_pet(["red", "dragon"])
539    output = capsys.readouterr().out
540
541    # THEN: A debug message should print
542    assert "You picked the cheerful red dragon." in output
543
544    # AND: The player should get treasure
545    assert adventure.PLAYER["inventory"]["gems"] == 20
546
547    # AND: The player should see a message about what happened
548    assert "10 gems" in output
B. In adventure.py modify DRAGONS#
Modify the dragon dictionaries in the DRAGONS list to add "treasure" tuples
for the "cheerful" and "lonely" dragons.
Need help?
- [ ]Add a- "treasure"key to the single dictionary in- DRAGONSlist for the- "cheerful"and- "lonely"dragons- [ ]The key should be- "treasure"
- [ ]The value should a tuple with two numbers representing the minimum and maximum amount of treasure. ie- (10, 20).
 
DRAGONS
50DRAGONS = [
51    {
52        "mood": "cheerful",
53        "treasure": (3, 15),
54    },
55    {
56        "mood": "grumpy",
57    },
58    {
59        "mood": "lonely",
60        "treasure": (8, 25),
61    },
62]
B. In adventure.py modify do_pet()#
In this section we’ll retrieve the range of possible treasure from the dragon
dictionary (if the "treasure" key exists) then use the two numbers in the
random.randint() function to get the number of gems to give the player.
Then we’ll add that number of gems to the player’s inventory and print a message about it.
Need help?
- [ ]Use the- .get()method on the- dragondictionary to get the value for the- "treasure"key and assign the result to- possible_treasure. Since not all dragons will have the- "treasure"key, use the second argument in- .get()to set the default value to- (0, 0).
- [ ]Pass the minimum and maximum numbers from- possible_treasureas arguments to the- random.randint()function and assign the results to- dragon["gems"].
- [ ]Use the- inventory_change()function to add- dragon["gems"]to the players inventory.
- [ ]Print a message like:- "The dragon's MOOD head gave you GEMS gems."
do_pet()
716def do_pet(args):
717    """Pet dragons"""
718
719    debug(f"Trying to pet: {args}")
720
721    # make sure they are somewhere they can pet dragons
722    if not place_can("pet"):
723        error("You can't do that here.")
724        return
725
726    # remove the expected words from args
727    for word in ["dragon", "head"]:
728        if word in args:
729            args.remove(word)
730
731    # make sure the player said what they want to pet
732    if not args:
733        error("What do you want to pet?")
734        return
735
736    color = args[0].lower()
737
738    # make sure they typed in a real color
739    if color not in COLORS:
740        error("I don't see a dragon that looks like that.")
741        return
742
743    # get the dragon info for this color
744    dragon = random.choice(DRAGONS)
745    dragon["color"] = color
746
747    debug(f"You picked the dragon's {dragon['mood']} {dragon['color']} head.")
748
749    # calculate the treasure
750    possible_treasure = dragon.get("treasure", (0, 0))
751    dragon["gems"] = random.randint(*possible_treasure)
752
753    # add the treasure to the players inventory
754    inventory_change("gems", dragon["gems"])
755
756    # print a message about gems
757    if dragon["gems"]:
758        write(f"The dragon's {dragon['mood']} head gave you {dragon['gems']} gems.")
Part 14.7: Damage#
In this section we’re going to write the code so that some of the dragons will cause the player damage.
In order to do this we’ll add a "damage" key to some of the dragon
dictionaries DRAGONS. The value will be a tuple containing the minimum and
maximum possible damage that a particular dragon might cause.
Demo
Then in the do_pet() function we’ll retrieve the range of possible damage
from the dragon dictionary, randomly pick a number in that range, then subtract
that amount the player health and print a message to tell the player what
happened.
A. In test_game.py modify test_do_pet_cheerful_dragon()#
In this section we’ll modify the test_do_pet_cheerful_dragon() test to make
sure that the player’s health does not change.
Need help?
1. After WHEN add AND: The player has a certain amount of health
…
- [ ]Set “health” value in the- PLAYERdictionary to a number like- 90
2. After THEN add AND: The player’s health should be the same
…
- [ ]Assert that- PLAYER["health"]is the same as it was before
3. Run your tests. They should pass.
test_do_pet_cheerful_dragon()
517def test_do_pet_cheerful_dragon(capsys):
518    # GIVEN: The player is in a place where they can pet a dragon
519    adventure.PLAYER["place"] = "cave"
520    adventure.PLACES["cave"] = {
521        "name": "A cave",
522        "can": ["pet"]
523    }
524
525    # AND: There is one color of dragon heads
526    adventure.COLORS = ["red"]
527
528    # AND: There is one dragon who gives you treasure
529    adventure.DRAGONS = [{
530        "mood": "cheerful",
531        "treasure": (10, 10),
532    }]
533
534    # AND: The player has some gems
535    adventure.PLAYER["inventory"] = {"gems": 10}
536
537    # AND: The player has a certain amount of health
538    adventure.PLAYER["health"] = 90
539
540    # WHEN: The player pets that head
541    do_pet(["red", "dragon"])
542    output = capsys.readouterr().out
543
544    # THEN: A debug message should print
545    assert "You picked the cheerful red dragon." in output
546
547    # AND: The player should get treasure
548    assert adventure.PLAYER["inventory"]["gems"] == 20
549
550    # AND: The player's health should be the same
551    assert adventure.PLAYER["health"] == 90
552
553    # AND: The player should see a message about what happened
554    assert "10 gems" in output
B. In test_game.py define test_do_pet_cranky_dragon()#
In this section we’ll define the test_do_pet_cranky_dragon() test. It will be
very similar to test_do_pet_cheerful_dragon(), except in the DRAGONS
dictionary we’ll add a dragon dictionary that has a "damage" key with a tuple
of two negative numbers and we’ll assert that PLAYER["health"] has decreased.
Need help?
1. GIVEN: The player is in a place where they can pet a dragon
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be a list containing the string- "pet"
2. AND: There is one color of dragon heads
…
- [ ]Assign- adventure.COLORSto a list containing one color
3. AND: There is one dragon who causes damage
…
- [ ]Assign- adventure.DRAGONSto a list containing one dictionary that contains:- [ ]the key- "mood"and the string- "cranky"for the value.
- [ ]the key- "damage"and a tuple with two negative numbers for the value
 
4. AND: The player has a certain amount of health
…
- [ ]Set- "health"in the- PLAYERdictionary to a number greater than- 0and less than- 100
5. AND: The player has some gems
…
- [ ]Set- "gems"in the- PLAYER["inventory"]dictionary to a positive number
6. WHEN: The player pets that head
…
- [ ]Call- do_pet()with a list that contains the same color that is in- COLORS
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
7. THEN: A debug message should print
…
- [ ]assert that an debug message like- "You picked the dragon's cranky red head."is in- output
8. AND: The player’s health should be reduced
…
- [ ]assert that the- PLAYER["health"]is less than it was before
9. AND: The player’s gems should not be changed
…
- [ ]assert that the- PLAYER["inventory"]["gems"]is the same as it was before
10. AND: The player should see a message about what happened
…
- [ ]assert that an debug message like- "caused you DAMAGE" damageis in- output
11. Run your tests. They should fail.
test_do_pet_cranky_dragon()
557def test_do_pet_cranky_dragon(capsys):
558    # GIVEN: The player is in a place where they can pet a dragon
559    adventure.PLAYER["place"] = "cave"
560    adventure.PLACES["cave"] = {
561        "name": "A cave",
562        "can": ["pet"]
563    }
564
565    # AND: There is one color of dragon heads
566    adventure.COLORS = ["red"]
567
568    # AND: There is one dragon who causes damage
569    adventure.DRAGONS = [{
570        "mood": "cranky",
571        "damage": (-10, -10),
572    }]
573
574    # AND: The player has a certain amount of health
575    adventure.PLAYER["health"] = 100
576
577    # AND: The player has some gems
578    adventure.PLAYER["inventory"] = {"gems": 10}
579
580    # WHEN: The player pets that head
581    do_pet(["red", "head"])
582    output = capsys.readouterr().out
583
584    # THEN: A debug message should print
585    assert "You picked the cranky red dragon." in output
586
587    # AND: The player's health should be reduced
588    assert adventure.PLAYER["health"] == 90
589
590    # AND: The player's gems should not be changed
591    assert adventure.PLAYER["inventory"]["gems"] == 10
592
593    # AND: The player should see a message about what happened
594    assert "-10 damage" in output
C. In adventure.py modify DRAGONS#
Modify the dragon dictionaries in the DRAGONS list to add "damage" tuples
for the "cranky" and "lonely" dragons.
Need help?
- [ ]Add a- "damage"key to the single dictionary in- DRAGONSlist for the- "cranky"and- "lonely"dragons- [ ]The key should be- "damage"
- [ ]The value should a tuple with two numbers representing the minimum and maximum amount of damage. ie- (-20, -10).
 
DRAGONS
50DRAGONS = [
51    {
52        "mood": "cheerful",
53        "treasure": (3, 15),
54    },
55    {
56        "mood": "grumpy",
57        "damage": (-15, -3),
58    },
59    {
60        "mood": "lonely",
61        "treasure": (8, 25),
62        "damage": (-25, -8),
63    },
64]
D. In adventure.py modify do_pet()#
In this section we’ll retrieve the range of possible damage from the dragon
dictionary (if the "damage" key exists) then use the two numbers in the
random.randint() function to get the number of gems to give the player.
Then we’ll add that number of gems to the player’s inventory and print a message about it.
Need help?
- [ ]Use the- .get()method on the- dragondictionary to get the value for the- "damage"key and assign the result to- possible_damage. Since not all dragons will have the- "damage"key, use the second argument in- .get()to set the default value to- (0, 0).
- [ ]Pass the minimum and maximum numbers from- possible_damageas arguments to the- random.randint()function and assign the results to- dragon["health"].
- [ ]Use the- health_change()function to subtract- dragon["health"]from the players health.
- [ ]Print a message like:- "The dragon's MOOD head caused you HEALTH damage."
do_pet()
718def do_pet(args):
719    """Pet dragons"""
720
721    debug(f"Trying to pet: {args}")
722
723    # make sure they are somewhere they can pet dragons
724    if not place_can("pet"):
725        error("You can't do that here.")
726        return
727
728    # remove the expected words from args
729    for word in ["dragon", "head"]:
730        if word in args:
731            args.remove(word)
732
733    # make sure the player said what they want to pet
734    if not args:
735        error("What do you want to pet?")
736        return
737
738    color = args[0].lower()
739
740    # make sure they typed in a real color
741    if color not in COLORS:
742        error("I don't see a dragon that looks like that.")
743        return
744
745    # get the dragon info for this color
746    dragon = random.choice(DRAGONS)
747    dragon["color"] = color
748
749    debug(f"You picked the dragon's {dragon['mood']} {dragon['color']} head.")
750
751    # calculate the treasure
752    possible_treasure = dragon.get("treasure", (0, 0))
753    dragon["gems"] = random.randint(*possible_treasure)
754
755    # calculate the damage
756    possible_damage = dragon.get("damage", (0, 0))
757    dragon["health"] = random.randint(*possible_damage)
758
759    # add the treasure to the players inventory
760    inventory_change("gems", dragon["gems"])
761
762    # reduce health
763    health_change(dragon["health"])
764
765    # print a message about gems
766    if dragon["gems"]:
767        write(f"The dragon's {dragon['mood']} head gave you {dragon['gems']} gems.")
768
769    # print a message about damage
770    if dragon["health"]:
771        write(f"The dragon's {dragon['mood']} head causes you {dragon['health']} damage.")
E. In test_game.py define test_do_pet_lonely_dragon()#
In this section we’ll define the test_do_pet_lonely_dragon() test. It will be
just like combining the test for a "cheerful" dragon and a "cranky" dragon.
That is, the dragon dictionary in DRAGONS should have both "treasure" and
"damage".
Need help?
1. GIVEN: The player is in a place where they can pet a dragon
…
- [ ]Change- PLAYERto put the player in a fake place
- [ ]Add a matching fake place dictionary to- PLACES. The- "can"key should be a list containing the string- "pet"
2. AND: There is one color of dragon heads
…
- [ ]Assign- adventure.COLORSto a list containing one color
3. AND: There is one dragon who causes damage and gives treasure
…
- [ ]Assign- adventure.DRAGONSto a list containing one dictionary that contains:- [ ]the key- "mood"and the string- "cranky"for the value.
- [ ]the key- "treasure"and a tuple with two positive numbers for the value
- [ ]the key- "damage"and a tuple with two negative numbers for the value
 
4. AND: The player has a certain amount of health
…
- [ ]Set- "health"in the- PLAYERdictionary to a number greater than- 0and less than- 100
5. AND: The player has some gems
…
- [ ]Set- "gems"in the- PLAYER["inventory"]dictionary to a positive number
6. WHEN: The player pets that head
…
- [ ]Call- do_pet()with a list that contains the same color that is in- COLORS
- [ ]Assign the results of- capsys.readouterr().outto the variable- output
7. THEN: A debug message should print
…
- [ ]assert that an debug message like- "You picked the dragon's lonely red head."is in- output
8. AND: The player’s health should be reduced
…
- [ ]assert that the- PLAYER["health"]is less than it was before
9. AND: The player should get treasure
…
- [ ]assert that the- PLAYER["inventory"]["gems"]is more than it was before
10. AND: The player should see a message about what happened
…
- [ ]assert that an debug message like samp- "caused you {HEALTH}" damageis in- output
11. Run your tests. They should pass.
test_do_pet_lonely_dragon()
597def test_do_pet_lonely_dragon(capsys):
598    # GIVEN: The player is in a place where they can pet a dragon
599    adventure.PLAYER["place"] = "cave"
600    adventure.PLACES["cave"] = {
601        "name": "A cave",
602        "can": ["pet"]
603    }
604
605    # AND: There is one color of dragon heads
606    adventure.COLORS = ["blue"]
607
608    # AND: There is one dragon who gives treasure and causes damage
609    adventure.DRAGONS = [{
610        "mood": "lonely",
611        "damage": (-10, -10),
612        "treasure": (20, 20),
613    }]
614
615    # AND: The player has a certain amount of health
616    adventure.PLAYER["health"] = 100
617
618    # AND: The player has a certain number of gems
619    adventure.PLAYER["gems"] = 10
620
621    # WHEN: The player pets that head
622    do_pet(["blue", "head"])
623    output = capsys.readouterr().out
624
625    # THEN: A debug message should print
626    assert "You picked the lonely blue dragon." in output
627
628    # AND: The player's health should be reduced
629    assert adventure.PLAYER["health"] == 90
630
631    # AND: The player should get treasure
632    assert adventure.PLAYER["inventory"]["gems"] > 10
633
634    # AND: The player should see a message about what happened
635    assert "20 gems" in output
636    assert "-10 damage" in output
Part 14.8: Pet the dragon#
In this section we’ll add a description of what happens when you pet a dragon’s head, pausing between each line to give it a sense of drama.
Demo
A. At the top of adventure.py: import sleep from time#
In order to add a pause for effect in between lines in the action description,
we’ll need to import the sleep function from the time module.
Need help?
- [ ]import the- sleepfunction from the- timemodule
Code
26from time import sleep
27
28from console import fg, fx
29from console.progress import ProgressBar
B. At the top of adventure.py: add DELAY#
At the top of your script where your other global variables are, add a new
global variable DELAY and set it to a number that will be the number of
seconds to pause for effect.
Need help?
- [ ]Set- DELAYto a number like- 1
DELAY
33WIDTH = 45
34
35MARGIN = 2
36
37DEBUG = True
38
39DELAY = 1.25
40
41MAX_HEALTH = 100
42
C. Modify do_pet(): add action description with delay#
In this section we’re going to add a description of what happens when the player pets the dragon’s head. To make it a little more exciting, we’ll split the description onto multiple strings in a tuple. Something like:
- "You slowly creep forward..."
- "...gingerly reach out your hand..."
- "...and gently pat the dragon's COLOR head."
- "..."
- "He blinks sleepy eyes and peers at you..."
Before printing the messages about gems and damage, we’ll iterate over the
sentences tuple. In each iteration we’ll print a blank line,
print the string, and call sleep() with the argument DELAY.
Finally we’ll print one blank line at the end.
Need help?
- [ ]Create a tuple (or list) assigned to- sentencesthat contains the three strings from above.
- [ ]Above the messages about gems and damage are printed, use a for loop to iterate over- sentenceswith the variable- text. In each iteration:- [ ]Print a blank line.
- [ ]Use the- write()function to print- text.
- [ ]Call- sleep()with the argument- DELAY.
 
- [ ]Print a blank line.
- [ ]Play your game and see how it looks!
do_pet()
748    # get the dragon info for this color
749    dragon = random.choice(DRAGONS)
750    dragon["color"] = color
751
752    debug(f"You picked the dragon's {dragon['mood']} {dragon['color']} head.")
753
754    # calculate the treasure
755    possible_treasure = dragon.get("treasure", (0, 0))
756    dragon["gems"] = random.randint(*possible_treasure)
757
758    # calculate the damage
759    possible_damage = dragon.get("damage", (0, 0))
760    dragon["health"] = random.randint(*possible_damage)
761
762    # add the treasure to the players inventory
763    inventory_change("gems", dragon["gems"])
764
765    # reduce health
766    health_change(dragon["health"])
767
768    sentences = (
769        "You slowly creep forward...",
770        "...gingerly reach out your hand...",
771        f"...and gently pat the dragon's {color} head.",
772        "...",
773        "He blinks sleepy eyes and peers at you...",
774    )
775
776    for text in sentences:
777        print()
778        write(text)
779        sleep(DELAY)
780
781    print()
782
783    # print a message about gems
784    if dragon["gems"]:
785        write(f"The dragon's {dragon['mood']} head gave you {dragon['gems']} gems.")
786
787    # print a message about damage
788    if dragon["health"]:
789        write(f"The dragon's {dragon['mood']} head causes you {dragon['health']} damage.")
790
791
D. At the top of test_game.py: set adventure.DELAY#
If you were to run your tests now, you would find that all of the
test_do_pet_* tests run an awful lot slower. That’s because do_pet() calls
sleep() in the tests just like it does in the game.
To avoid this problem, simply set adventure.DELAY to 0 near the top of your
test file. Unlike the changes that we make in a GIVEN portion of a test, we
only need to set adventure.DELAY once. Put it just under where you assign all
of the *_STATE global variables.
Need help?
- [ ]Find where you assign- PLAYER_STATE,- DEBUG_STATE, etc. Just under that set- adventure.DELAYto- 0
- [ ]Run your tests. They should pass and be as fast as usual.
Code
25def setup_module(module):
26    """Initialize global state variables. Should be run once."""
27    global PLAYER_STATE, PLACES_STATE, ITEMS_STATE, DEBUG_STATE
28
29    PLAYER_STATE = deepcopy(adventure.PLAYER)
30    PLACES_STATE = deepcopy(adventure.PLACES)
31    ITEMS_STATE = deepcopy(adventure.ITEMS)
32    DEBUG_STATE = True
33    adventure.DELAY = 0
Part 14.9: Better messages#
In this section we’ll make nicer and more detailed messages for when each dragon causes damage or gives treasure.
To do this we’ll add a "message" key to each dragon dictionary in DRAGONS,
which will have the end of the message for a value.
Demo
For example, say we want the message to be:
"The happy green dragon thinks you're great and gives you 100 gems!"
Then the "message" value would be:
"thinks you're great and gives you {gems} gems!"
It’s important to note that value should contain the f-string style variables
{gems} and/or {damage}, but it should not actually be an f-string.
That way we can attach it to the beginning of the message in do_pet(), then
call the .format() method on the resulting string to fill in all the
variable values.
A. Modify test_do_pet_*_dragon(): add "message" to DRAGONS#
In this section we’ll modify the three test_do_pet_*_dragon() tests to add
the "message" key to the single dictionary in DRAGONS with the value bing a
string that contains the f-string style variables {gems} and/or {health}.
Need help?
1. Modify AND: There is one dragon who gives you treasure
…
- [ ]Add the key- "message"to the single dictionary in- DRAGONSwith the value:- [ ]A string that is not an f-string but contains the f-string style variables- {gems}and/or- {health}.- It should be the part of the message that comes after - "The dragon's MOOD COLOR head"and describes what the dragon does after the player pets it.
 
2. Run your tests. They should pass.
test_do_pet_cheerful_dragon()
518def test_do_pet_cheerful_dragon(capsys):
519    # GIVEN: The player is in a place where they can pet a dragon
520    adventure.PLAYER["place"] = "cave"
521    adventure.PLACES["cave"] = {
522        "name": "A cave",
523        "can": ["pet"]
524    }
525
526    # AND: There is one color of dragon heads
527    adventure.COLORS = ["red"]
528
529    # AND: There is one dragon who gives you treasure
530    adventure.DRAGONS = [{
531        "mood": "cheerful",
532        "treasure": (10, 10),
533        "message": "likes you and gives you {gems} gems.",
534    }]
test_do_pet_cranky_dragon()
559def test_do_pet_cranky_dragon(capsys):
560    # GIVEN: The player is in a place where they can pet a dragon
561    adventure.PLAYER["place"] = "cave"
562    adventure.PLACES["cave"] = {
563        "name": "A cave",
564        "can": ["pet"]
565    }
566
567    # AND: There is one color of dragon heads
568    adventure.COLORS = ["red"]
569
570    # AND: There is one dragon who causes damage
571    adventure.DRAGONS = [{
572        "mood": "cranky",
573        "damage": (-10, -10),
574        "message": "pushes you away causing you {health} damage",
575    }]
test_do_pet_lonely_dragon()
600def test_do_pet_lonely_dragon(capsys):
601    # GIVEN: The player is in a place where they can pet a dragon
602    adventure.PLAYER["place"] = "cave"
603    adventure.PLACES["cave"] = {
604        "name": "A cave",
605        "can": ["pet"]
606    }
607
608    # AND: There is one color of dragon heads
609    adventure.COLORS = ["blue"]
610
611    # AND: There is one dragon who gives treasure and causes damage
612    adventure.DRAGONS = [{
613        "mood": "lonely",
614        "damage": (-10, -10),
615        "treasure": (20, 20),
616        "message": "throws {gems} gems at you causing you {health} damage",
617    }]
B. In adventure.py modify DRAGONS: add "message"#
Modify the dragon dictionaries in the DRAGONS list to add "message" string
for all three dragons.
Need help?
- [ ]Add the key- "message"to the all three dragon dictionaries in- DRAGONSwith the value:- [ ]A string that is not an f-string but contains the f-string style variables- {gems}and/or- {health}.- It should be the part of the message that comes after - "The dragon's MOOD COLOR head "and describes what the dragon does after the player pets it.
 
DRAGONS
53DRAGONS = [
54    {
55        "mood": "cheerful",
56        "treasure": (3, 15),
57        "message": "thinks you're adorable! He gives you {gems} gems!"
58    },
59    {
60        "mood": "grumpy",
61        "damage": (-15, -3),
62        "message": (
63            "wants to be left alone. The heat from his mighty sigh "
64            "singes your hair, costing you {health} in health."
65        ),
66    },
67    {
68        "mood": "lonely",
69        "treasure": (8, 25),
70        "damage": (-25, -8),
71        "message": (
72            "is just SO happy to see you! He gives you a whopping "
73            "{gems} gems! Then he hugs you, squeezes you, and calls "
74            "you George... costing you {health} in health."
75        )
76    },
77]
C. Modify do_pet(): print the message#
Now we’ll combine the beginning of the message
"The dragon's {mood} {color} head " with the string from dragon["message"].
Since the dragon dictionary already has the information about mood,
color, and gems and/or health, we can call .format() on the resulting
string and pass the whole dictionary as keyword arguments. That will fill in
all those variables with their corresponding values from the dictionary.
Then we can replace the lines where we print the two old messages with a line printing the new message, wrapped.
Need help?
- [ ]Remove the lines where you print the messages about gems and damage.
- [ ]Concatonate the string- "The dragon's {mood} {color} head "with- dragon["message"]and assign the result to the variable- tpl.
- [ ]Call the- .format()method on- tpland pass all key-value pairs from the- dragondictionary as keyword arguments. Assign the result to- text.
- [ ]Use the- wrap()function to print- text.
- [ ]Run your tests. They should pass.
do_pet()
758    # get the dragon info for this color
759    dragon = random.choice(DRAGONS)
760    dragon["color"] = color
761
762    debug(f"You picked the dragon's {dragon['mood']} {dragon['color']} head.")
763
764    # calculate the treasure
765    possible_treasure = dragon.get("treasure", (0, 0))
766    dragon["gems"] = random.randint(*possible_treasure)
767
768    # calculate the damage
769    possible_damage = dragon.get("damage", (0, 0))
770    dragon["health"] = random.randint(*possible_damage)
771
772    # add the treasure to the players inventory
773    inventory_change("gems", dragon["gems"])
774
775    # reduce health
776    health_change(dragon["health"])
777
778    sentences = (
779        "You slowly creep forward...",
780        "...gingerly reach out your hand...",
781        f"...and gently pat the dragon's {color} head.",
782        "...",
783        "He blinks sleepy eyes and peers at you...",
784    )
785
786    for text in sentences:
787        print()
788        write(text)
789        sleep(DELAY)
790
791    print()
792
793    # generate the message
794    tpl = "The dragon's {mood} {color} head " + dragon["message"]
795    text = tpl.format(**dragon)
796    wrap(text)
Part 14.10: Add dragon#
Finally, we’ll add the dragon itself as an item in your cave so the player can examine it to find out the colors of each dragon head.
Demo
A. In adventure.py modify ITEMS#
In your ITEMS dictionary, add a "dragon" key. Use the global variable
COLORS in the description to list the colors of the dragon heads.
ITEMS
214    "dragon": {
215        "key": "dragon",
216        "name": "a three-headed dragon",
217        "description": (
218            f"A colossal dragon with heads of {', '.join(COLORS[0:-1])}, "
219            f"and {COLORS[2]}."
220        ),
221    },
B. In adventure.py modify PLACES#
COLORS
147    "cave": {
148        "key": "cave",
149        "name": "A cave",
150        "north": "hill",
151        "description": (
152            "Your footsteps echo as you step into the vast cavern.",
153            "Shafts of sunlight slice through the gloom, playing against the "
154            "landscape of glittering treasure.",
155            "Resting atop a mound of gold, a colossal dragon rests curled up snugly. "
156            "Its three enormous heads snore softly, each in turn.",
157        ),
158        "items": ["dragon"],
159        "can": ["pet"],
160    },