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?
  1. [ ] Import the do_pet function

  2. [ ] Add test_do_pet() function with one parameter capsys

  3. [ ] Call do_pet() with an empty list as an argument

  4. [ ] Assign the results of capsys.readouterr().out to the variable output

  5. [ ] Write an assert statement that checks that the debug message "Trying to pet: []" is in output

  6. [ ] Run your tests. They should fail.

Code
Listing 788 test_game.py#
 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)
Listing 789 test_game.py#
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?
  1. [ ] Add a do_pet() function with one parameter args

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

  3. [ ] Run your tests again. They should now pass.

do_pet()
Listing 790 adventure.py#
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?
  1. [ ] Add an elif that checks if command is "pet".

    • [ ] if so, call do_pet() and pass args.

main()
Listing 791 adventure.py in 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 PLAYER to 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().out to 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()
Listing 792 test_game.py#
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?
  1. [ ] 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()
Listing 793 adventure.py#
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?
  1. [ ] Add a place called cave with the "can" key set to a list that includes the string "pet"

  2. [ ] Modify the "east", "west", "north", and "south" key(s) of your other places so that the player can get to the cave.

PLACES
Listing 794 adventure.py#
 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 PLAYER to 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().out to 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()
Listing 795 test_game.py#
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?
  1. [ ] Check if args is empty. If so:

    • [ ] Print an error message like "What do you want to pet?"

    • [ ] return

do_pet()
Listing 796 adventure.py#
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 PLAYER to 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().out to 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()
Listing 797 test_game.py#
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:

  1. [ ] 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

  2. [ ] Run your tests. They should pass.

do_pet()
Listing 798 adventure.py#
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.COLORS to a list of colors

1. AND: The player is in a place where they can pet things

  • [ ] Change PLAYER to 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().out to 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()
Listing 799 test_game.py#
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
Listing 800 adventure.py#
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?
  1. [ ] Assign the first item from args to the variable color

  2. [ ] Check to make sure that color is 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()
Listing 801 adventure.py#
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 PLAYER to 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.COLORS to a list containing one color

3. AND: There is one dragon

  • [ ] Assign adventure.DRAGONS to 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().out to 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()
Listing 802 test_game.py#
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?
  1. [ ] import the random module

Code
Listing 803 adventure.py#
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?
  1. [ ] Create global variable DRAGONS and 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
Listing 804 adventure.py#
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?
  1. [ ] Call random.choice() with the argument DRAGONS and assign it to the variable dragon.

  2. [ ] Set dragon["color"] to color

  3. [ ] Print a debug message like "You picked the dragon's MOOD COLOR head."

do_pet()
Listing 805 adventure.py#
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 DRAGONS for

    • [ ] 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 PLAYER inventory to a number

3. After THEN add AND: The player should get treasure

  • [ ] assert that the gems in PLAYER inventory 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()
Listing 806 test_game.py#
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 DRAGONS list 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
Listing 807 adventure.py#
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?
  1. [ ] Use the .get() method on the dragon dictionary 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).

  2. [ ] Pass the minimum and maximum numbers from possible_treasure as arguments to the random.randint() function and assign the results to dragon["gems"].

  3. [ ] Use the inventory_change() function to add dragon["gems"] to the players inventory.

  4. [ ] Print a message like: "The dragon's MOOD head gave you GEMS gems."

do_pet()
Listing 808 adventure.py#
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 PLAYER dictionary 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()
Listing 809 test_game.py#
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 PLAYER to 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.COLORS to a list containing one color

3. AND: There is one dragon who causes damage

  • [ ] Assign adventure.DRAGONS to 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 PLAYER dictionary to a number greater than 0 and 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().out to 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" damage is in output

11. Run your tests. They should fail.

test_do_pet_cranky_dragon()
Listing 810 test_game.py#
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 DRAGONS list 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
Listing 811 adventure.py#
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?
  1. [ ] Use the .get() method on the dragon dictionary 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).

  2. [ ] Pass the minimum and maximum numbers from possible_damage as arguments to the random.randint() function and assign the results to dragon["health"].

  3. [ ] Use the health_change() function to subtract dragon["health"] from the players health.

  4. [ ] Print a message like: "The dragon's MOOD head caused you HEALTH damage."

do_pet()
Listing 812 adventure.py#
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 PLAYER to 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.COLORS to a list containing one color

3. AND: There is one dragon who causes damage and gives treasure

  • [ ] Assign adventure.DRAGONS to 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 PLAYER dictionary to a number greater than 0 and 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().out to 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}" damage is in output

11. Run your tests. They should pass.

test_do_pet_lonely_dragon()
Listing 813 test_game.py#
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?
  1. [ ] import the sleep function from the time module

Code
Listing 814 adventure.py#
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?
  1. [ ] Set DELAY to a number like 1

DELAY
Listing 815 adventure.py#
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?
  1. [ ] Create a tuple (or list) assigned to sentences that contains the three strings from above.

  2. [ ] Above the messages about gems and damage are printed, use a for loop to iterate over sentences with the variable text. In each iteration:

    • [ ] Print a blank line.

    • [ ] Use the write() function to print text.

    • [ ] Call sleep() with the argument DELAY.

  3. [ ] Print a blank line.

  4. [ ] Play your game and see how it looks!

do_pet()
Listing 816 adventure.py: 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?
  1. [ ] Find where you assign PLAYER_STATE, DEBUG_STATE, etc. Just under that set adventure.DELAY to 0

  2. [ ] Run your tests. They should pass and be as fast as usual.

Code
Listing 817 test_game.py#
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 DRAGONS with 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()
Listing 818 test_game.py#
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()
Listing 819 test_game.py#
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()
Listing 820 test_game.py#
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 DRAGONS with 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
Listing 821 adventure.py#
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?
  1. [ ] Remove the lines where you print the messages about gems and damage.

  2. [ ] Concatonate the string "The dragon's {mood} {color} head " with dragon["message"] and assign the result to the variable tpl.

  3. [ ] Call the .format() method on tpl and pass all key-value pairs from the dragon dictionary as keyword arguments. Assign the result to text.

  4. [ ] Use the wrap() function to print text.

  5. [ ] Run your tests. They should pass.

do_pet()
Listing 822 adventure.py: 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
Listing 823 adventure.py#
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
Listing 824 adventure.py#
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    },