Part 12: Read things#

In this section we’ll add the read command, which the player can use to read a clue from the book item.

In this section we’ll also start following an approach to coding called TDD, or Test-Driven Development. When following this process, you write your test first, then write the code that makes it pass.

This technique has many advantages. You are forced to be very deliberate about exactly what you are trying to accomplish, which tends to lead to clearer thinking and cleaner code. You can be more confident that your code is working as intended and that it won’t break in the future without you noticing.

Part 12.1: Add command#

In this section we’ll be adding the read command.

A. In test_game.py define test_do_read()#

First we’ll write the test, which we expect to fail.

Demo
  1. [ ] Import the do_read function

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

  3. [ ] Call do_read() 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 read: []" is in output

  6. [ ] Run your tests, either at the command line or in VS Code. Since we haven’t written do_read() yet, you will get an error.

Code
 1from copy import deepcopy
 2
 3import pytest
 4
 5import adventure
 6from adventure import (
 7    debug,
 8    do_drop,
 9    do_read,
10    error,
11    header,
12    inventory_change,
13    is_for_sale,
14    place_has,
15    player_has,
16    write,
17)
18
19
Listing 747 test_game.py#
170def test_do_read(capsys):
171    do_read([])
172
173    output = capsys.readouterr().out
174
175    assert "Trying to read: []" in output, \
176        "Debug message should be in output"

B. In adventure.py define do_read()#

Now we’ll write the code to make it work.

Demo
  1. [ ] Add a do_read() function with one parameter args

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

  3. [ ] Run your test again. It should now pass.

Code
Listing 748 adventure.py#
522def do_read(args):
523    """Read an item with a message."""
524
525    debug(f"Trying to read: {args}")
526

C. In adventure.py modify main()#

Finally, we’ll add the code to make the command work in the game.

  1. [ ] Add an elif that checks if command is "read".

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

Demo
Code
527def main():
528    header("Welcome!")
529
530    while True:
531        debug(f"You are at: {PLAYER['place']}")
532
533        reply = input(fg.cyan("> ")).strip()
534        args = reply.split()
535
536        if not args:
537            continue
538
539        command = args.pop(0)
540        debug(f"Command: {command!r}, args: {args!r}")
541
542        if command in ("q", "quit", "exit"):
543            do_quit()
544
545        elif command in ("shop"):
546            do_shop()
547
548        elif command in ("g", "go"):
549            do_go(args)
550
551        elif command in ("x", "exam", "examine"):
552            do_examine(args)
553
554        elif command in ("l", "look"):
555            do_look()
556
557        elif command in ("t", "take", "grab"):
558            do_take(args)
559
560        elif command in ("i", "inventory"):
561            do_inventory()
562
563        elif command in ("r", "read"):
564            do_read(args)
565
566        elif command == "drop":
567            do_drop(args)
568
569        elif command == "buy":
570            do_buy(args)
571
572        else:
573            error("No such command.")
574            continue
575
576        # print a blank line no matter what
577        print()
578

Part 12.2: Ensure item#

In this section we’ll make sure that the player entered the item that they want to read.

Demo

A. In test_game.py modify test_do_read()#

Now we’ll add an assertion to check the output for an error message.

  1. [ ] Change the name of test_do_read() to test_do_read_no_args()

  2. [ ] Add an assertion that checks if "Error What do you want to read?" is in output

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

Demo
Code
Listing 749 test_game.py#
170def test_do_read_no_args(capsys):
171    do_read([])
172
173    output = capsys.readouterr().out
174
175    assert "Trying to read: []" in output, \
176        "Debug message should be in output"
177
178    assert "Error What do you want to read?" in output, \
179        "User error should be in output"

B. In adventure.py modify do_read()#

Now we’ll write the code to make our test pass.

  1. [ ] Check if args is falsy. If it is, use the error() function to print an error that says: "What do you want to read?" then return

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

Demo
Code
Listing 750 test_game.py#
522def do_read(args):
523    """Read an item with a message."""
524
525    debug(f"Trying to read: {args}")
526
527    # make sure the player said what they want to read
528    if not args:
529        error("What do you want to read?")
530        return
531

Part 12.3: Ensure item is here#

In this section we’ll make sure the item that the player typed is either in this place or in inventory.

Demo

A. In test_game.py define test_do_read_missing_item()#

In this section we’ll write a new test to make sure that the item is either in this place or in inventory.

Demo
  1. [ ] Add test_do_read_missing_item() function with the parameter capsys

  2. [ ] Call do_read() with a list and a any string that is not an item key for an argument

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

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

  5. [ ] Write an assert statement that checks that the message "Sorry, I don't know what this is'" is in output

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

Code
Listing 751 test_game.py#
181def test_do_read_missing_item(capsys):
182    do_read(["missing"])
183
184    output = capsys.readouterr().out
185
186    assert "Trying to read: ['missing']" in output, \
187        "Debug message should be in output"
188
189    assert "Error Sorry, I don't know what this is: 'missing'" in output, \
190        "User error should be in output"

B. In adventure.py modify do_read()#

Now we’ll add the code to make the test pass.

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

  2. [ ] Write an if statement that checks if either the place or player has the item. If not:

    • [ ] Use the error() function to print a message like: "Sorry, I don't know what this is: name'"

    • [ ] return

  3. [ ] Run your test again. It should pass.

Code
Listing 752 adventure.py#
522def do_read(args):
523    """Read an item with a message."""
524
525    debug(f"Trying to read: {args}")
526
527    # make sure the player said what they want to read
528    if not args:
529        error("What do you want to read?")
530        return
531
532    # get the item entered by the user and make it lowercase
533    name = args[0].lower()
534
535    # make sure the item is in this place or in the players inventory
536    if not (place_has(name) or player_has(name)):
537        error(f"Sorry, I don't know what this is: {name!r}.")
538        return
539

Part 12.4: Ensure item is readable#

In this section we’ll make sure that the item the player wants to be read can, in fact, be read.

Demo

A. In test_game.py define test_do_read_unreadable_item()#

In this section we’ll be adding a test for when the player tries to read an item that cannot be read. It should:

  • [ ] Add a fake unreadable item dictionary to the current place. An item is readable if it has a "message" key, so for a fake item all we really need is an empty dictionary.

  • [ ] Call do_read() with the arguments that would be passed to it if the player had typed "read FAKE_THING".

  • [ ] Check the output for the appropriate error message

Demo
  1. [ ] import the place_add function

  2. [ ] Add test_do_read_unreadable_item() function with the parameter capsys

  3. [ ] Add a fake item to adventure.ITEMS with the key of your choice

  4. [ ] Use the place_add() function to add your fake item to the current place

  5. [ ] Call do_read() with a list containing your new key as the argument

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

  7. [ ] Write an assert statement that checks that the message "Sorry, I don't can't read 'key'" is in output

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

Code
Listing 753 test_game.py#
193def test_do_read_unreadable_item(capsys):
194    adventure.ITEMS["your mind"] = {"name": "Your mind"}
195    place_add("your mind")
196
197    do_read(["your mind"])
198    output = capsys.readouterr().out
199
200    assert "Error Sorry, I can't read 'your mind'" in output, \
201        "User error should be in output"

B. In adventure.py modify do_read()#

Now we’ll write the code to make the test pass.

Demo
  1. [ ] Use the get_item() function to get the item dictionary and assign it to the variable item

  2. [ ] Check if the "message" key is in the item dictionary. If not:

    • [ ] Use the error() function to print an error message like: "Sorry, I can't read 'name'"

    • [ ] return

  3. [ ] Run your test again. It should pass.

Code
Listing 754 adventure.py#
522def do_read(args):
523    """Read an item with a message."""
524
525    debug(f"Trying to read: {args}")
526
527    # make sure the player said what they want to read
528    if not args:
529        error("What do you want to read?")
530        return
531
532    # get the item entered by the user and make it lowercase
533    name = args[0].lower()
534
535    # make sure the item is in this place or in the players inventory
536    if not (place_has(name) or player_has(name)):
537        error(f"Sorry, I don't know what this is: {name!r}.")
538        return
539
540    # get the item dictionary
541    item = get_item(name)
542
543    # make sure it is an item you can read
544    if "writing" not in item:
545        error(f"Sorry, I can't read {name!r}.")
546        return
547

Part 12.5: Read things#

In this section we’ll finally provide a readable item and the read command will read it.

A. In test_game.py define test_do_read_in_place()#

In this section we’ll write a new test where we’ll set up a fake item with "message" and "title" keys containing the text to be read and add it to the current place. Then we’ll call do_read()and check for the expected output.

Demo
  1. [ ] Add a function test_do_read_in_place()

  2. [ ] Create a dictionary representing a fake item item with the keys "title" and "message" and whatever text you’d like for the values.

  3. [ ] Use the place_add() function to add it to the current place

  4. [ ] Call do_read() with the key for your fake item

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

  6. [ ] Write an assert statement that the title is in output

  7. [ ] Write an assert statement that the message is in output

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

Code
Listing 755 test_game.py#
204def test_do_read_in_place(capsys):
205    title = "After studying your palm I see..."
206    message = "The break in your line of fate may indicate a change in location or career"
207
208    adventure.ITEMS["your palm"] = {
209        "name": "Your palm",
210        "writing": {
211            "title": title,
212            "message": message,
213        },
214    }
215
216    place_add("your palm")
217
218    do_read(["your palm"])
219    output = capsys.readouterr().out
220
221    assert "After studying your palm I see..." in output, \
222        "The writing title {title!r} should be in output"
223
224    assert "The break in your line of fate" in output, \
225        "The writing message {message!r} should be in output"
226
227

B. In adventure.py modify do_read()#

Now we’ll write the code to actually read the item.

Demo
  1. [ ] If the "title" key exists in the item dictionary, Use the header() function to print that value. Otherwise print something like "It reads...".

  2. [ ] Use the wrap() function to print the "message" value from the item dictionary.

Code
Listing 756 adventure.py#
539def do_read(args):
540    """Read an item with a message."""
541
542    debug(f"Trying to read: {args}")
543
544    # make sure the player said what they want to read
545    if not args:
546        error("What do you want to read?")
547        return
548
549    # get the item entered by the user and make it lowercase
550    name = args[0].lower()
551
552    # make sure the item is in this place or in the players inventory
553    if not (place_has(name) or player_has(name)):
554        error(f"Sorry, I don't know what this is: {name!r}.")
555        return
556
557    # get the item dictionary
558    item = get_item(name)
559
560    # make sure it is an item you can read
561    if "writing" not in item:
562        error(f"Sorry, I can't read {name!r}.")
563        return
564
565    # get the information to read
566    writing = item["writing"]
567
568    # print the item header
569    header(writing["title"])
570
571    # print the quantity message
572    wrap(writing.get("message"))
573

C. Modify ITEMS: add to "book"#

Now we’ll finally give the player something to read.

  1. [ ] In the "book" dictionary in ITEMS, add the key "title" with whatever text you’d like

  2. [ ] In the "book" dictionary in ITEMS, add the key "message" with whatever text you’d like

Demo
Code
Listing 757 adventure.py#
 94    "book": {
 95        "key": "book",
 96        "can_take": True,
 97        "name": "a book",
 98        "description": (
 99            "A hefty leather-bound tome open to an interesting passage."
100        ),
101        "writing": {
102            "title": (
103                "The book is open to a page that reads:"
104            ),
105            "message": (
106                "At the edge of the woods is a cave that is home to a three "
107                "headed dragon, each with a different temperament. "
108
109                "Legend says that if you happen upon the dragon sleeping, the "
110                "brave may pet one of its three heads. "
111
112                "Choose the right head and you will be rewarded with great "
113                "fortunes. "
114
115                "But beware, choose poorly and it will surely mean your doom! "
116            ),
117        },
118    },

D. In test_game.py define test_do_read_in_inventory()#

Can you write a test_do_read_in_inventory() function on your own, to test that you can read a book if it is in your inventory but not in the current place?

Code
Listing 758 test_game.py#
228def test_do_read_in_inventory(capsys):
229    title = "After studying the leaves I see..."
230    message = "Your future is uncertain."
231
232    adventure.ITEMS["tea leaves"] = {
233        "name": "Tea Leaves",
234        "writing": {
235            "title": title,
236            "message": message,
237        },
238    }
239
240    inventory_change("tea leaves")
241
242    do_read(["tea leaves"])
243    output = capsys.readouterr().out
244
245    assert "After studying the leaves I see..." in output, \
246        "The writing title {title!r} should be in output"
247
248    assert "Your future is uncertain" in output, \
249        "The writing message {message!r} should be in output"

Part 12.6: Indent message#

In this section we’re going to indent the message part of the text an extra level.

Demo

A. In test_game.py modify test_do_read_in_inventory()#

In this section we’re going to modify our test_do_read_in_inventory() function to test that the message has been indented an extra level.

  1. [ ] Call .splitlines() on output and assign it to the variable lines

  2. [ ] Write an assert statement that checks if the last item in lines equals your fake items message with 4 spaces at the beginning. (Assuming your message is short enough.)

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

Demo

Tip

When you run pytest at the command line, you can run a single test by adding ::test_name after the filename. For example, here I used pytest test_game.py::test_do_read_in_inventory.

Code
Listing 759 test_game.py#
278def test_do_read_in_inventory(capsys):
279    title = "After studying the leaves I see..."
280    message = "Your future is uncertain."
281
282    adventure.ITEMS["tea leaves"] = {
283        "name": "Tea Leaves",
284        "writing": {
285            "title": title,
286            "message": message,
287        },
288    }
289
290    inventory_change("tea leaves")
291
292    do_read(["tea leaves"])
293    output = capsys.readouterr().out
294    lines = output.splitlines()
295
296    assert "After studying the leaves I see..." in output, \
297        "The writing title {title!r} should be in output"
298
299    assert lines[-1] == "      Your future is uncertain.", \
300        "The writing message {message!r} should be indented an extra level."

B. In test_game.py define test_wrap()#

Before we change any code in do_read(), we’re actually going to modify the wrap() function to optionally add indentation. And since we don’t have any tests for wrap() yet, we’ll one before we change it.

Since we’re testing pre-existing behavior here, this is one we’ll expect to pass.

Demo
  1. [ ] Add a function test_wrap() that takes one parameter capsys

  2. [ ] Call wrap() with a string that is longer than the WIDTH in your game.

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

  4. [ ] Call .splitlines() on output and assign it to the variable lines

  5. [ ] Write an assert statement that tests that the length of lines is greater than 1

  6. [ ] Write an assert statement that tests that output contains the first few words of the text argument.

  7. [ ] Write an assert statement that tests that output ends with the last few words of the text argument, followed by a newline.

  8. [ ] Make sure that each string in lines starts with two, and no more than two, spaces.

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

Code
Listing 760 test_game.py#
 92def test_wrap(capsys):
 93    wrap(
 94        "I pass through many Me's in the course of my day, "
 95        "each one selfish with his time. The Lying-in-Bed "
 96        "me and the Enjoying-the-Hot-Shower Me are particularly "
 97        "selfish. The Late Me loathes the pair of them."
 98    )
 99    output = capsys.readouterr().out
100    lines = output.splitlines()
101
102    assert "I pass through" in output, \
103        "wrap() should print the text indented."
104
105    assert output.endswith("pair of them.\n"), \
106        "wrap() should print the text"
107
108    assert len(lines) > 1, "wrap() should break long text onto multiple lines"
109
110    assert all([line.startswith("  ") for line in lines]), \
111        "wrap() should indent all lines"
112
113    assert all([line[2] != " " for line in lines]), \
114        "wrap() should indent all lines 2 spaces"
115

C. In test_game.py define test_wrap_with_indent()#

Now that we have the normal wrap() behavior tested, we’ll add a (failing) test for the new behavior with the optional indent parameter we will be adding.

This is going to be nearly identical to the test_wrap() function, except we’ll be adding the keyword argument indent=2 when we call wrap(), then testing that the lines are indented to 4 spaces instead of 2.

Demo
  1. [ ] Add a function test_wrap_with_indent() that takes one parameter capsys

  2. [ ] Call wrap() with a string that is longer than the WIDTH in your game, followed by a keyword argument indent with a value of 2.

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

  4. [ ] Call .splitlines() on output and assign it to the variable lines

  5. [ ] Write an assert statement that tests that the length of lines is greater than 1

  6. [ ] Write an assert statement that tests that output contains the first few words of the text argument.

  7. [ ] Write an assert statement that tests that output ends with the last few words of the text argument, followed by a newline.

  8. [ ] Make sure that each string in lines starts with four, and no more than four, spaces.

  9. [ ] Run your test. (Or however many extra spaces you would like.) It should fail.

Code
Listing 761 test_game.py#
116def test_wrap_with_indent(capsys):
117    wrap(
118        "To the absolutist in every craftsman, "
119        "each imperfection is a failure; "
120        "to the practitioner, obsession with "
121        "perfection seems a perception for failure.",
122        indent=2,
123    )
124    output = capsys.readouterr().out
125    lines = output.splitlines()
126
127    assert "To the absolutist" in output, \
128        "wrap() should print the text."
129
130    assert output.endswith("perception for failure.\n"), \
131        "wrap() should print the text"
132
133    assert len(lines) > 1, "wrap() should break long text onto multiple lines"
134
135    assert all([line.startswith("    ") for line in lines]), \
136        "wrap() should indent all lines"
137
138    assert all([line[4] != " " for line in lines]), \
139        "wrap() should indent all lines 4 spaces"
140

D. In adventure.py modify wrap()#

We’re finally ready to modify the wrap() function to handle indention for us.

  1. [ ] Add an optional indent parameter to wrap with a default value of 1

  2. [ ] When calculating margin, multiply the existing value by indent

  3. [ ] Run your test_wrap_with_indent() test. It should pass.

  4. [ ] Run your test_wrap() test. It should pass.

Demo

Tip

When you run pytest at the command line, you can use the -k KEYWORD flag to run all tests with names that contain KEYWORD. For example, here I use pytest test_game.py -k test_wrap to run both test_wrap() and test_wrap_with_indent(), but no other tests.

Code
Listing 762 test_game.py#
136def wrap(text, indent=1):
137    """Print wrapped and indented text."""
138
139    # calculate the indentation
140    margin = (MARGIN * " ") * indent
141
142    # wrap the text
143    paragraph = textwrap.fill(
144        text,
145        WIDTH,
146        initial_indent=margin,
147        subsequent_indent=margin,
148    )
149
150    # print the wrapped text
151    print(paragraph)
152

E. In adventure.py modify do_read()#

Now we’re finally ready to modify our do_read() function to use the new indent keyword argument in wrap().

  1. [ ] When you print the item message by calling wrap() add a keyword argument indent with a value of 2. (Or more, to your taste. Just be sure you update your test.)

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

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

Demo
Code
Listing 763 test_game.py#
541def do_read(args):
542    """Read an item with a message."""
543
544    debug(f"Trying to read: {args}")
545
546    # make sure the player said what they want to read
547    if not args:
548        error("What do you want to read?")
549        return
550
551    # get the item entered by the user and make it lowercase
552    name = args[0].lower()
553
554    # make sure the item is in this place or in the players inventory
555    if not (place_has(name) or player_has(name)):
556        error(f"Sorry, I don't know what this is: {name!r}.")
557        return
558
559    # get the item dictionary
560    item = get_item(name)
561
562    # make sure it is an item you can read
563    if "writing" not in item:
564        error(f"Sorry, I can't read {name!r}.")
565        return
566
567    # get the information to read
568    writing = item["writing"]
569
570    # print the item header
571    header(writing["title"])
572
573    # print the item message
574    wrap(writing.get("message"), indent=3)
575

Part 12.7: Allow for stanzas#

In this section we’ll add functionality to break up a long message (in particular, our book message) into multiple stanzas.

To accomplish this, we’ll modify wrap() so that for its text argument it can take either a string or an iterable (a tuple or a list) of strings. If text is a string, it should print just the same as it does now. If text is a tuple or a list, each item should be wrapped separately and printed with a blank line between each one.

Demo

A. In test_game.py modify test_do_read_in_place()#

In this section we’ll modify the test_do_read_in_place() test so that the "message" in our fake item is either a tuple or a list, where each item represents a stanza. Then we’ll add an assert statement to check that there are two "\n" before one of our stanzas.

We’ll leave the test_do_read_in_inventory() test alone, which will make sure that it still works if message is a string.

Demo
  1. [ ] Modify the value corresponding to the "message" key in your fake item dictionary to be either a tuple or a list of strings with multiple items.

  2. [ ] Add an assert statement that checks to make sure that output contains two blank lines, followed by the number of indentation spaces, followed by the first few words of one of your message items.

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

Code
Listing 764 test_game.py#
278def test_do_read_in_place(capsys):
279    title = "After studying your palm I see..."
280    message = (
281        "The break in your line of fate may indicate "
282        "a change in location or career.",
283
284        "You have more than one life line, which may "
285        "indicate you are a cat",
286    )
287
288    adventure.ITEMS["your palm"] = {
289        "name": "Your palm",
290        "title": title,
291        "message": message,
292    }
293
294    place_add("your palm")
295
296    do_read(["your palm"])
297    output = capsys.readouterr().out
298
299    assert "After studying your palm I see..." in output, \
300        "The writing title {title!r} should be in output"
301
302    assert "The break in your line of fate" in output, \
303        "The writing message {message!r} should be in output"
304
305    assert "\n\n      You have more than" in output, \
306        "When message is an iterable, there should be two lines between each item"
307
308

B. In test_game.py define test_wrap_with_iterable()#

Once again, the real heavy lifting will be done in the wrap() function. So before we write any code we’ll write a new wrap() test for when text is an iterable.

Demo
  1. [ ] Add a function test_wrap_with_iterable() with one parameter capsys

  2. [ ] Set the variable message to a list or a tuple of strings. (Consider using a few lyrics of a short song or rhyme.)

  3. [ ] Call the wrap() function with the argument message

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

  5. [ ] Write an assert statement that makes sure part of one of our message items is in output

  6. [ ] Write an assert statement to make sure that a parenthesis is not in output if message is a tuple, or that a square bracket is not in output if message is a list. This is to make sure that we are not mistakenly printing the entire iterable instead of each string in the iterable.

  7. [ ] Add an assert statement that checks to make sure that output contains two blank lines, followed by the number of indentation spaces, followed by the first few words of one of your message items.

  8. [ ] Run the test. It should fail.

Code
Listing 765 test_game.py#
141def test_wrap_with_iterable(capsys):
142    message = (
143        "The exercise for centering oneself is a simple one.",
144
145        "Stop thinking of what you intend to do. Stop thinking of what you "
146        "have just done. Then stop thinking that you have stopped thinking of "
147        "those things.",
148
149        "Then you will find the now. The time that stretches eternal, and is "
150        "really the only time there is.",
151    )
152    wrap(message)
153
154    output = capsys.readouterr().out
155
156    assert "The exercise for centering oneself" in output, \
157        "wrap() should print the text."
158
159    assert "(" not in output, \
160        "wrap() should not contain parenthesis representing a tuple"
161
162    assert "\n\n  Stop thinking" in output, \
163        "There should be two newlines between each printed message item"
164

C. In adventure.py modify wrap()#

Now we are ready to change the wrap() function.

First we’ll turn text into a one-item iterable if it was a string, that way we always have something that we can safely iterate over.

Then we’ll iterate over each string in our new text iterable, wrap it the same way we already do, and append it to a list called blocks.

Finally, we’ll print the blocks list with two "\n" between each one.

Demo

Note

We don’t need to make any changes to do_read() for this feature, since it already calls wrap() with whatever was in the item dictionary for "message". So once you’re done with this, all your tests should pass.

  1. [ ] Write an if statement to check if text is a string using the isinstance() function.

    • [ ] If so, make a list or tuple containing text and assign it to the same variable, text.

  2. [ ] Make an empty list and assign it to the variable blocks

  3. [ ] Use a for loop to iterate over text using the variable name stanza

    • [ ] Indent the line(s) where you assign paragraph to be inside your for loop

    • [ ] In your call to textwrap.fill(), instead of use stanza for the first argument instead of text.

    • [ ] Append paragraph to blocks

  4. [ ] You can either:

    • [ ] Use argument unpacking to send all of the items in the blocks list as separate arguments to the print() function, and the keyword argument sep to print two newlines between each argument.

    • [ ] Join the blocks list using two newlines as the delimiter, then print the result.

  5. [ ] Run your test_wrap_with_iterable() test. It should pass.

  6. [ ] Run your test_do_read_in_place() test. It should pass.

  7. [ ] Run all your tests. They should pass.

Code
Listing 766 adventure.py#
134def wrap(text, indent=1):
135    """Print wrapped and indented text."""
136
137    # calculate the indentation
138    margin = (MARGIN * " ") * indent
139
140    # if a string was passed, turn it into a single item tuple
141    if isinstance(text, str):
142        text = (text,)
143
144    # make an empty list for the wrapped blocks
145    blocks = []
146
147    # iterate over items
148    for stanza in text:
149
150        # wrap the text
151        paragraph = textwrap.fill(
152            stanza,
153            WIDTH,
154            initial_indent=margin,
155            subsequent_indent=margin,
156        )
157
158        blocks.append(paragraph)
159
160    # print the wrapped text
161    print(*blocks, sep="\n\n")
162

D. In adventure.py modify ITEMS#

Finally, change the "message" in your "book" item to be a list or a tuple of strings.

And now that we’re all literate, feel free to scatter scrolls, signs and sticky notes all over your game!

Code
Listing 767 adventure.py#
 94    "book": {
 95        "key": "book",
 96        "can_take": True,
 97        "name": "a book",
 98        "description": (
 99            "A hefty leather-bound tome open to an interesting passage."
100        ),
101        "title": (
102            "The book is open to a page that reads:"
103        ),
104        "message": (
105            "At the edge of the woods is a cave that is home to a three "
106            "headed dragon, each with a different temperament. ",
107
108            "Legend says that if you happen upon the dragon sleeping, the "
109            "brave may pet one of its three heads. ",
110
111            "Choose the right head and you will be rewarded with great "
112            "fortunes. ",
113
114            "But beware, choose poorly and it will surely mean your doom!",
115        ),
116    },