Part 13: Health#

In this section we’ll add player health between 1 - 100 with a health progress bar on the inventory command.

Part 13.1: Add health_change()#

In this section we’ll start the health_change() function which will work very much like inventory_change(). It should:

  • Take one int argument: amount.

  • Add the amount to PLAYER["health"].

A. In test_game.py define test_health_change()#

Write the test_health_change() function to test health_change().

Need help?
  1. Import health_change

  2. Add the function test_health_change()

  3. GIVEN: the player has health

    Set PLAYER["health"] to a positive number between 1 - 100

  4. WHEN: you call health_change() with a positive number

  5. *THEN: a positive number should be added to player health

    assert that PLAYER["health"] is now correct

  6. Run your test. It should fail.

Code
Listing 768 test_game.py#
 6from adventure import (
 7    debug,
 8    do_drop,
 9    do_read,
10    error,
11    header,
12    health_change,
13    inventory_change,
14    is_for_sale,
15    place_has,
16    player_has,
17    place_add,
18    wrap,
19    write,
20)
Listing 769 test_game.py#
52def test_health_change():
53    # GIVEN: The player has some health
54    adventure.PLAYER["health"] = 50
55
56    # WHEN: You call health_change() with a positive value
57    health_change(10)
58
59    # THEN: The amount should be added to player health
60    assert adventure.PLAYER["health"] == 60, \
61        "a positive number should be added to player health"
62
63

B. In adventure.py define health_change()#

Write the health_change() function.

Need help?
  1. [ ] Add the function health_change() with one argument amount

  2. [ ] Add amount to PLAYER["health"]

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

Code
Listing 770 adventure.py#
222def health_change(amount: int):
223    """Add the following (positive or negative) amount to health, but limit to 0-100"""
224    PLAYER["health"] += amount
225
226

Part 13.2: Parameterize the test#

In this section we’ll modify the test_health_change() function to use parametrization. This allows us to use the same test for several different test cases which are stored and run as a list of arguments to a single test function.

If you’re not already familiar with parameterization, see Pytest Tests > Parametrization.

A. In test_game.py modify test_health_change(): parameterize#

In this section we will extract the values that we expect to be different in new test cases and turn them into four parameters and variables: start, amount, result and message.

At the end of this section we will have a parameratized test with exactly one test case representing the original test. Functionally, nothing will change but we’ll be set up to more easily add new test cases.

  1. Change the following values to variables. (Either keep the original line commented out or otherwise make note the original values.)

    Listing 771 test_game.py#
     1def test_health_change():
     2    # GIVEN: The player has some health
     3    adventure.PLAYER["health"] = 50
     4
     5    # WHEN: You call health_change()
     6    health_change(10)
     7
     8    # THEN: The player health should be adjusted
     9    assert adventure.PLAYER["health"] == 60, \
    10      "the player health should be adjusted"
    
    • Under GIVEN: Change the value assigned to PLAYER["health"] to the variable start.

    • Under WHEN: Change the argument passed to health_change() the variable amount

    • Under THEN: In the assert statement change the value that should equal PLAYER["health"] to the variable result

    • Under THEN: Change the assert message to the variable message

      If you don’t already have an assert message add one that is similar to the THEN description. ie:

      "a positive number should be added to player health"

  2. Add the four variables (start, amount, result, message) as parameters to the test_health_change() function

  3. Immediately above the def line call @pytest.mark.parametrize() with the following arguments:

    • A list containing the name of all four variables in the same order as above

    • A list of tuples with each tuple on its own line

      • The first tuple should contain the values extracted from step #1 in the same order as their cooresponding parameters: start, amount, result, message.

  4. Change your GIVEN/WHEN/THEN comments to be more generic so that they can apply to all test cases. For example, change:

    # THEN: The amount should be added to player health

    To:

    # THEN: The player health should be adjusted

  5. Run your test. It should pass.

Code
Listing 772 test_game.py#
52@pytest.mark.parametrize(
53    ["start", "amount", "result", "message"], [
54        (50, 10, 60, "a positive number should be added"),
55    ]
56)
57def test_health_change(start, amount, result, message):
58    # GIVEN: The player has some health
59    adventure.PLAYER["health"] = start
60
61    # WHEN: You call health_change()
62    health_change(amount)
63
64    # THEN: The player health should be adjusted
65    assert adventure.PLAYER["health"] == result, message

B. In test_game.py above test_health_change(): Add another test case#

In this section we are going to add a test case for passing a negative value to health_change() to effectively subtract that amount from player health.

If we were to write this as a non-parameratized test it would look something like:

Listing 773 test_game.py#
64def test_health_change_subtract():
65    # GIVEN: The player has some health
66    adventure.PLAYER["health"] = 50
67
68    # WHEN: You call health_change() with a negative value
69    health_change(-10)
70
71    # THEN: The amount should be subtracted from player health
72    assert adventure.PLAYER["health"] == -40, \
73        "a negative number should be subtracted from player health"

Instead of writing this test though, we will add a test case tuple that contains the start, amount, result and message values that we would otherwise put in a test_health_change_subtract() test.

  1. [ ] Add a new tuple to the list of test case tuples that contains values for each of the four parameters:

    Parameter

    Value

    start

    a positive int

    amount

    a negative int

    result

    start - amount

    message

    a str describing this test case

  2. [ ] Run your test. It should pass.

Code
Listing 774 test_game.py#
52@pytest.mark.parametrize(
53    ["start", "amount", "result", "message"], [
54        (50, 10, 60, "a positive number should be added"),
55        (50, -10, 40, "a negative number should be subtracted"),
56    ]
57)
58def test_health_change(start, amount, result, message):
59    # GIVEN: The player has some health
60    adventure.PLAYER["health"] = start
61
62    # WHEN: You call health_change()
63    health_change(amount)
64
65    # THEN: The player health should be adjusted
66    assert adventure.PLAYER["health"] == result, message

Part 13.3: Add health limits#

In this section we’re going to modify the health_change() function so that PLAYER["health"] is always between 0 and 100.

A. In test_game.py above test_health_change(): ensure health > 0#

In this section we will add a test case to ensure that even if start - amount is less than zero, player health is set to zero instead of a negative number

If we were to write this as a non-parameratized test, it would look something like:

test_health_change_minimum_health()
Listing 775 test_game.py#
64def test_health_change_minimum_health():
65    # GIVEN: The player has some health
66    adventure.PLAYER["health"] = 20
67
68    # WHEN: You call health_change() with a negative number
69    #       which would make player health less than zero
70    health_change(-30)
71
72    # THEN: The player health should be zero
73    assert adventure.PLAYER["health"] == 0, \
74        "the minimum health should be 0"
  1. [ ] Add a new tuple to the list of test case tuples that contains values for each of the four parameters:

    Parameter

    Value

    start

    a positive int

    amount

    a negative int that would make player health less than 0

    result

    0

    message

    a str describing this test case

  2. [ ] Run your test. It should fail.

Code
Listing 776 test_game.py#
53@pytest.mark.parametrize(
54    ["start", "amount", "result", "message"], [
55        (50, 10, 60, "a positive number should be added"),
56        (50, -10, 40, "a negative number should be subtracted"),
57        (20, -30, 0, "the minimum health should be 0"),

B. In adventure.py modify health_change()#

In this section we will modify health_change() to make the above test case pass.

  1. [ ] At the end of the function, after PLAYER["health"] is changed, check if PLAYER["health"] is less than zero

    • [ ] if so, set PLAYER["health"] to zero

  2. [ ] Run your tests. They should pass

Code
Listing 777 adventure.py#
224def health_change(amount: int):
225    """Add the following (positive or negative) amount to health, but limit to 0-100"""
226    PLAYER["health"] += amount
227
228    # don't let health go below zero
229    if PLAYER["health"] < 0:
230        PLAYER["health"] = 0
231

C. At the top of adventure.py#

In this section we’ll add a global variable MAX_HEALTH to keep track of the maximum value for PLAYER["health"].

  1. [ ] Add global variable MAX_HEALTH and set it to 100

Code
Listing 778 adventure.py#
24import textwrap
25
26from console import fg, fx
27
28WIDTH = 45
29
30MARGIN = 2
31
32DEBUG = True
33
34MAX_HEALTH = 100
35

D. In test_game.py above test_health_change(): ensure MAX_HEALTH#

In this section we’ll add a test case to ensure that even if start + result is greater than MAX_HEALTH, player health will be set to MAX_HEALTH.

If we were to write this as a non-parameratized test it would look something like:

test_health_change_maximum_health()
Listing 779 test_game.py#
64def test_health_change_maximum_health():
65    # GIVEN: The player has some health
66    adventure.PLAYER["health"] = 90
67
68    # WHEN: You call health_change() with a positive number
69    #       which would make player health more than the maximum
70    health_change(20)
71
72    # THEN: The player health should be MAX_HEALTH
73    assert adventure.PLAYER["health"] == MAX_HEALTH, \
74        f"the maximum health should be {MAX_HEALTH}"
  1. [ ] Import MAX_HEALTH

  2. [ ] Add a new tuple to the list of test case tuples that contains values for each of the four parameters:

    Parameter

    Value

    start

    a positive int

    amount

    a positive int that would make player health more than 100

    result

    MAX_HEALTH

    message

    a str describing this test case

  3. [ ] Run your test. It should fail.

Code
Listing 780 test_game.py#
 6from adventure import (
 7    debug,
 8    do_drop,
 9    do_read,
10    error,
11    header,
12    health_change,
13    inventory_change,
14    is_for_sale,
15    place_has,
16    player_has,
17    place_add,
18    wrap,
19    write,
20    MAX_HEALTH,
21)
Listing 781 test_game.py#
53@pytest.mark.parametrize(
54    ["start", "amount", "result", "message"], [
55        (50, 10, 60, "a positive number should be added"),
56        (50, -10, 40, "a negative number should be subtracted"),
57        (20, -30, 0, "the minimum health should be 0"),
58        (90, 20, MAX_HEALTH, f"the max health should be {MAX_HEALTH}"),
59    ]
60)
61def test_health_change(start, amount, result, message):

E. In adventure.py modify health_change()#

In this section we will modify health_change() to make the above test case pass.

  1. [ ] At the end of the function, check if PLAYER["health"] is greater than MAX_HEALTH

    • [ ] if so, set PLAYER["health"] to MAX_HEALTH

  2. [ ] Run your tests. They should pass

Code
Listing 782 adventure.py#
224def health_change(amount: int):
225    """Add the following (positive or negative) amount to health, but limit to 0-100"""
226    PLAYER["health"] += amount
227
228    # don't let health go below zero
229    if PLAYER["health"] < 0:
230        PLAYER["health"] = 0
231
232    # cap health
233    if PLAYER["health"] > MAX_HEALTH:
234        PLAYER["health"] = MAX_HEALTH
235
236

Part 13.4: UX Changes#

In this section we’ll add a few changes to integrate health with the game itself.

A. In adventure.py modify PLAYER#

  1. [ ] Add a "health" key to the PLAYER dictionary with a value of 100

Code
Listing 783 adventure.py#
45PLAYER = {
46    "place": "home",
47    "inventory": {"gems": 50},
48    "health": MAX_HEALTH,
49}
50

B. At the top of adventure.py: Add ProgressBar#

We’re going to use the progress bar feature of the console library to add a health bar to the inventory command.

In this section we’ll add a global variable BAR which will be set to a ProgressBar() object. We’ll use this later to print the progress bar.

  1. [ ] Import ProgressBar from console.progress

  2. [ ] Create a new global variable BAR and set it to a new ProgressBar() object with the following keyword arguments:

    Keyword

    Value

    Why

    total

    MAX_HEALTH + 0.1

    prevent dimming of bar at 100%

    clear_left

    False

    prevent removal of "Health " text to left of bar

    width

    WIDTH - len("Health") - len("100%")

    make bar width WIDTH minus the length of the other text on the same line

Note

From the command line you can run python -m console.progress to see a demo of the various progress bar styles.

Feel free to play around and pick your own style.

Code
Listing 784 adventure.py#
24import textwrap
25
26from console import fg, fx
27from console.progress import ProgressBar
28
29WIDTH = 45
30
31MARGIN = 2
32
33DEBUG = True
34
35MAX_HEALTH = 100
36
37BAR = ProgressBar(
38    total=(MAX_HEALTH + 0.1),
39    width=(WIDTH - len("Health") - len("100%")),
40    clear_left=False,
41)
42

C. In adventure.py define health_bar()#

In this section we’ll add a function health_bar() which will print the health bar.

  1. [ ] Write a function health_bar()

  2. [ ] In use the write() command to print:

    • [ ] "Health"

    • [ ] the value returned when you call BAR() and pass the argument PLAYER["health"]

Code
Listing 785 adventure.py#
198def health_bar():
199    """Print a progress bar showing player health"""
200    print()
201    write(f"Health {BAR(PLAYER['health'])}")
202
203

D. In adventure.py modify do_inventory()#

In this section we’ll call health_bar() in the do_inventory() function to print the health bar.

  1. [ ] At the beginning of do_inventory() call health_bar()

Demo
Code
Listing 786 adventure.py#
505def do_inventory():
506    """Show the players inventory"""
507
508    debug("Trying to show inventory.")
509
510    health_bar()
511
512    header("Inventory")
513
514    if not PLAYER["inventory"]:
515        write("Empty.")
516        return
517
518    for name, qty in PLAYER["inventory"].items():
519        item = get_item(name)
520        write(f"(x{qty:>2})  {item['name']}")
521
522    print()
523
524

E. In adventure.py modify main()#

In this section we’ll quit the game if player health is out of health.

  1. [ ] At the very end of the main() function, check to make sure that the player still has health

  2. [ ] If not print something like "Game over" and call quit()

Code
Listing 787 adventure.py#
642def main():
643    header("Welcome!")
644
645    while True:
646        debug(f"You are at: {PLAYER['place']}")
647
648        reply = input(fg.cyan("> ")).strip()
649        args = reply.split()
650
651        if not args:
652            continue
653
654        command = args.pop(0)
655        debug(f"Command: {command!r}, args: {args!r}")
656
657        if command in ("q", "quit", "exit"):
658            do_quit()
659
660        elif command in ("shop"):
661            do_shop()
662
663        elif command in ("g", "go"):
664            do_go(args)
665
666        elif command in ("x", "exam", "examine"):
667            do_examine(args)
668
669        elif command in ("l", "look"):
670            do_look()
671
672        elif command in ("t", "take", "grab"):
673            do_take(args)
674
675        elif command in ("i", "inventory"):
676            do_inventory()
677
678        elif command in ("r", "read"):
679            do_read(args)
680
681        elif command == "drop":
682            do_drop(args)
683
684        elif command == "buy":
685            do_buy(args)
686
687        else:
688            error("No such command.")
689            continue
690
691        # print a blank line no matter what
692        print()
693
694        # exit the game if player has no health
695        if not PLAYER["health"]:
696            write("Game over.\n")
697            quit()
698
699