Collecting my attempts to improve at tech, art, and life

Python Interactive Fiction - 01 Handling a Single Round

Tags: python interactive-fiction coolnamehere

Series: Python Interactive Fiction

I think the next step is to write the code for a single round of the game. We’ll limit ourselves to Scene 1 to stay focused.

Presenting a Scene to the user

First you want to show the description. Start a new Python file in your favorite editor, or in IDLE with the menu command “File” -> “New”.

# ifiction.py
#  - An interactive fiction game

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print description

The \ character at the end of lines 6 and 7 tells Python the string is continuing to the next line. Python would complain at us if we left that out. Let’s run the script and see what we get.

Save your new file as ifiction.py and run it. Press the F5 key to run the script if you are using IDLE.

$ python ifiction.py

You are standing in a field. To the north of you are some mountains, to the ea
st of you is a forest, to the west of you is a cave, and to the south of you i
s a valley.

This may not be happening for you, but when I run the script the description text gets cut off at inconvenient points. What happens next is a little more advanced than I was planning to show you, but that is just going to bug me too much if I don’t fix it now.

Wrapping text with the textwrap module

One of Python’s charms is the fact that it has a huge standard library. This means that a lot of things you would like to do have already been written and included for free. That’s why some folks say that Python comes with “batteries included.” The standard library is a collection of modules with useful features and functions. I am just concerned with the fill function from the textwrap module right now, because I want the text of the description wrapped so that no words get cut off.

You aren’t automatically carrying all of those libraries around in your script, though. You need to use the import command to make the functions in a library available to your program.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

Notice that I had to use the module name as part of my call to fill. That is because Python needs to know where it can find the fill function, and for library functions it uses modulename followed by a dot (.) and then the function. You will be seeing a lot more of the dot operator as your Python knowledge expands.

There are ways to import the function in a way which removes that requirement, but for now I will stick with the more explicit version because it is easier for us to know where we found the function.

See what it looks like now?

$ python ifiction.py
You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.

Of course, if the wrapping text isn’t an issue for you, feel free to leave out the textwrap related code completely.

Back to the game: paths

Now for the paths. We could just print the paths and make the user type in the full path to go anywhere, but that would be unkind. What we want is an easy way to show the list of paths and say “You picked path #1: Go to the mountains”.

Python’s list type is the perfect way to do this. A Python list is a collection of values - they could be literals or other variables - that holds each value in order. This lets you ask for the third item in a list, or ask for each value in order. Let’s start by creating and displaying the paths.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

for path in paths:
    print path

What does it look like now?

$ python ifiction.py
You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
Go to the mountains
Go into the forest
Go into the cave
Go to the valley

I want to do a bit of formatting to break things up, but you get the idea of what is going on. We have created a variable called paths to hold our list of paths. What can I say? I like my variable names to be obvious. You can recognize a list by the square brackets []. Items in the list are separated by commas. I like to put each list item on a line by itself, using indentation to show that we are looking inside the list. Little things like this make your code easier to read, which gets very important as your program grows.

The for loop

Okay, I need to take the next few ideas slowly, because I have put a lot of important new concepts in two lines of code.

for is one of Python’s looping control structures. for path in paths: is going step to through each item in the list paths. That part is straightforward. Another nice thing about Python code is that you can usually tell what’s going on just by looking at it.

It also creates a variable called path which will hold the value of the current item in the list. The first time through the list, path is set to “Go to the mountains”, the second time through path is set to “Go into the forest”, and so on.

What is Python going to do with path? That is decided in the indented line after the colon : character.

for path in paths:
    print path

I kept it simple for now. All we do is print this path to the screen and move on. Do you see the extra indentation, though? Python uses indentation to know what code is supposed to be executed within the loop. If I try to describe it, I’m just going to make things more confusing than they really are. Just remember this:

An indent without a control statement is an error in Python, except for special cases like the way I defined description and paths. It doesn’t really matter how much you indent for a block, but it must be consistent throughout your program. The common standard suggested by Python creator Guido van Rossum is four spaces per block, with no tab characters used.

for <item> in <list> is useful, and it will probably be the most common tool in your kit for examining every item in a list. It doesn’t quite work for our exact needs today, though. We want to build a menu with an easy value for the user to enter. This will use the range() function combined with the [] operator for accessing list members.

range is a simple function which returns a list of whole numbers in a certain range. It normally takes a single argument: the upper edge of the range. All the numbers in the list will be less than the upper edge. Here is a simple example:

>>> range(3)
[0, 1, 2]

range can take additional arguments to set the starting number and the step size, but this is all we want for now. Oh, notice that the numbering starts at zero. This is going to be very useful, for reasons which will become clear in a few moments.

I have shown you how to look at a complete list, as well as how to look at each item in a list one at a time. How do you look at a single item in the list? You use []. What do they call that, anyways? I can’t really tell from the Python docs. Let’s call it the indexing operation. Why “operation?” Because we’re doing something, but not with a function. Why “indexing?” Because we will be using a specific value to get at the item, sort of like using the index or table of contents in a book.

So let’s try it out. It is easy to use the indexing operation. Add the left bracket, the index number of the item you want, and then the right bracket. Go back to the Python shell and try it out for yourself, getting the value at index 1 of a list:

>>> items = [ 'apples', 'chocolate bugs', 'bananas']
>>> items[1]
'chocolate bugs'

That was a little confusing, wasn’t it? We were expecting apples but got chocolate bugs instead You would think that the index would be easy: the first item would be at index 1, the second at index 2, and so on. Unfortunately, that’s not the wayindexes work in Python. Numbering starts with the first item being zero.

>>> items[0]
'apples'

Zero-based indexing is one of those language features that’s there for historical reasons. It made perfect sense a long time ago in another language, but now it just serves to confuse newcomers and create a lot of “off-by-one” errors. You may want to use a mental trick for reducing confusion: think of the index as the distance from the first item. The first item is the first item, so the distance is zero: items[0]. The next item is one away from the first item, so the distance is one: items[1]. And so on.

Or you could just subtract one from the number you’re thinking of and get on with it. Things that are shortcuts for me could just be useless clutter for you. I’m happy as long as you remember that list index numbers start at zero.

So where do the index numbers stop? You could count the items in the list code by hand and work with that number, but that is far too much work. Use Python’s built-in len function. len is blissfully simple. You hand it a list, and it tells you how many items are in the list. Try it yourself if you still have that shell open:

>>> len(items)
3

There are three items in the list, so the index starts at zero and ends at two. len works perfectly with range, which hands you a list of numbers starting at zero and ending at one less than the upper edge. Back to the shell, where we’ll step through our list using ’len’, ‘range’, and list indexing.

>>> for index in range(len(items)):
...     print index, items[index]
...
0 apples
1 chocolate bugs
2 bananas

Oh, I didn’t mention blocks in the shell, did I? When Python thinks you are in a block, it prints ... instead of >>>. Indent by hitting spaces (or I just use the tab key when I’m in the shell). Hitting Enter on a line with no code tells the Python shell that you are done with the block and it’s time to execute.

Just so you know - indexing operation is just something I came up with after looking around on the Web a little bit. That is not the official name for [], and I’ll be updating this section as soon as I find out what that name is.

This has been a quick crash course through list handling in Python. Let’s apply what we’ve learned about lists to our interactive story.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

for i in range(0, len(paths)):
    path = paths[i]
    menu_item = i + 1
    print "\t", menu_item, path

I used i + 1 instead of i because … well, Python may count from zero but most people count from one. We’re writing this for people, not for Python.

We didn’t really need to set up each path and menu_item as a variable, but I thought it would make things easier to read than print "\t", i+1, paths[i]. You want to aim for readabality when you are starting out or you will quickly become lost. Actually, it’s a good idea to aim for readability all the way through your programming life. It will make your code easier to maintain. Besides, putting these values in a variable leaves room for us to change our mind about path and menu_item are constructed in the future.

Run the script and see what you get.

You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
  1 Go to the mountains
  2 Go into the forest
  3 Go into the cave
  4 Go to the valley

It’s starting to look like something! Now go take a break for a minute. I threw a lot of information at you all at once, and you may still need to process it. You at least need to look at something besides a computer monitor for a few seconds and shake your fingers loose. It’s good for you.

Getting the user’s selection

I am pleased that we have the scene description code working, but user input is still missing. All we need is raw_input, which we encountered in the initial Python Babysteps. Add a line to get user input and another line to show the result.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

for i in range(0, len(paths)):
    path = paths[i]
    menu_item = i + 1
    print "\t", menu_item, path

choice = raw_input("Make a selection: ")
print "Choice", i, "-", paths[i-1]

Running this code is very exciting indeed:

You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
  1 Go to the mountains
  2 Go into the forest
  3 Go into the cave
  4 Go to the valley
Make a selection: 3
Choice 3 - Go into the cave

Quitting the game

Our specification mentioned that users may quit the game at any point, so we should add the code to make that possible. Normal choices are numbers and they start at one, so let’s take the easy way out and say that zero quits the game. The if selection control structures can be used to recognize the quit command.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

for i in range(0, len(paths)):
    path = paths[i]
    menu_item = i + 1
    print "\t", menu_item, path

print "\t(0 Quit)"
choice = int( raw_input("Make a selection: ")

if choice == 0:
    print "Good bye!"
else:
    print "Choice", choice, "-", paths[choice-1]

A little note about being careful. I spent five minutes debugging the above code. I kept getting Choice 3 Go into the cave, for every non-zero choice I entered. Turns out that I had cut and pasted some debugging code from earlier which was using the i variable. i was last set to 3, so that’s what Python kept printing for me. It can be very easy to get distracted while writing code, and although Python can catch a lot of errors, you must keep an eye out for little mistakes like that. Once I replace i with choice in the last line, everything was happy.

Now, why did I use the int function on the user input? Keyboard input comes to you in the form of a String, which is a different type than numbers. If we want to be able to use the input as an index for the paths list, we need a way to turn that String into an integer, or whole number. This is exactly what int does. What happens when the user entry can’t be turned into a number? That’s part of the next topic.

Ensuring valid choices

User input needs to be in the form of a number. Not only that, but that number needs to be a valid index for paths. If either of these turns out to be false, Python panics. Let’s explore this in the shell. As a special treat, I’ll show you a glimpse at making functions in Python.

>>> def get_index():
...     list = [ 10, 20, 30 ]
...     prompt = "Pick a number (0 - 2): "
...     index = int( raw_input(prompt) )
...     print list[index]
...
>>>

If you make a mistake, hit Enter twice to end the function definition and start over. Don’t forget your indentation! Incidentally, I chose to put the prompt in its own variable because all of those parentheses on the same line were making me a little dizzy.

Test get_index with a valid number first.

>>> get_index()
Pick a number (0 - 2): 0
10

What happens when you enter a number that’s too big? Try it and see.

>>> get_index()
Pick a number (0 - 2): 20
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 5, in get_index
IndexError: list index out of range

How about when you enter something that’s not a number again? Once again, TIAS (Try It And See).

>>> get_index()
Pick a number (0 - 2): banana
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 4, in get_index
ValueError: invalid literal for int(): banana

Python raises an exception at you whenever it encounters a situation it can’t handle on its own. An exception is a special type of object that is specially used for errors, accidents, or plain old weird events in your program. You can plan for them, though, and try to handle them when they happen. That is going to require a new kind of block: The try block. This sort of block is easier to understand in the context of a full program, so let’s go back to ifiction.py. We’ll start with catching every kind of exception in one go:

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

for i in range(0, len(paths)):
    path = paths[i]
    menu_item = i + 1
    print "\t", menu_item, path

print "\t(0 Quit)"

try:
    choice = int( raw_input("Make a selection: ") )

    if choice == 0:
        print "Good bye!"
    else:
        print "Choice", choice, "-", paths[choice-1]
except:
    print choice, "is not a valid selection!"

A try block usually has at least 2 parts:

We can specify what exceptions we would expect to see, but that is more than I want to look at right now. We’ll just put up a single except block that will catch every exception raised within the try block. We can test the code by entering a number that is too large.

You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
        1 Go to the mountains
        2 Go into the forest
        3 Go into the cave
        4 Go to the valley
        (0 Quit)
Make a selection: 20
Choice 20 - 20 is not a valid selection!

This is odd-looking. Our error message prints at exactly the spot where the exceptional situation was encountered: trying to access paths[19]. Well, at least I’ve illustrated exactly how dynamic Python is. It doesn’t look ahead to see if something bad is going to happen, so it has to trust us to know when to look for an error. I think it would be a little cleaner to put the choice description in its own variable before printing it. That way we get an exception before we try to give normal feedback to the user.

try:
    choice = int( raw_input("Make a selection: ") )

    if choice == 0:
        print "Good bye!"
    else:
        next_step = paths[choice-1]
        print "Choice", choice, "-", next_step
except:
    print choice, "is not a valid selection!"

Our code is starting to get a little long to show the whole thing for every little change, so I have decided to focus on the chunk of code that is being modified. Anyways, you can see that I have made a new next_step variable. Python will raise the exception about bad indexing here, instead of in the middle of printing out feedback. We have also made the code more readable in the process, which is a nice thing.

You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
        1 Go to the mountains
        2 Go into the forest
        3 Go into the cave
        4 Go to the valley
        (0 Quit)
Make a selection: 20
20 is not a valid selection!

Our input code handles bad indexes. Test the code again by entering a non-number for choice.

You are standing in a field. To the north of you are some mountains,
to the east of you is a forest, to the west of you is a cave, and to
the south of you is a valley.
        1 Go to the mountains
        2 Go into the forest
        3 Go into the cave
        4 Go to the valley
        (0 Quit)
Make a selection: steak and eggs
Traceback (most recent call last):
  File "ifiction.py", line 35, in ?
    print choice, "is not a valid selection!"
NameError: name 'choice' is not defined

Oops. I raised a whole new exception because choice isn’t defined until after it’s converted to a number, but I referred to it in the except block. Another intermediate variable will save us from that error.

try:
    choice = raw_input("Make a selection: ")
    menu_selection = int(choice)

    if menu_selection == 0:
        print "Good bye!"
    else:
        index = menu_selection - 1
        next_step = paths[ index ]
        print "Choice", menu_selection, "-", next_step
except:
    print choice, "is not a valid selection!"

There are several very small changes here. choice now refers only to the raw user input, and we have created a new variable menu_selection to hold the choice converted to an integer. This means we had to adjust the variable names where we were really talking about the number the user provided and not the keystrokes. You might have noticed that I created an index variable in the else block. This is a personal taste thing. I often start out using a raw expression and later replace it with a variable when I think it would make my meaning clearer. The “start sloppy and refine as you go” approach happens to work for me, but use whatever tactic you are most comfortable with.

Make a selection: an iced coffee would good right now
an iced coffee would good right now is not a valid selection!

Right. We can recognize bad input from the user. What do we want to do about it? The best approach may be to continue asking the user for input until we get something acceptable for the next step that she wants to take. We can use a simple while loop to handle this.

# Keep asking for input until we have a valid choice for the next step
next_step = None
while next_step == None:
    try:
        choice = raw_input("Make a selection: ")
        menu_selection = int(choice)

        if menu_selection == 0:
            next_step = "quit"
        else:
            index = menu_selection - 1
            next_step = paths[ index ]
            print "Choice", choice, "-", next_step
    except:
        print choice, "is not a valid selection!"

print "You decided to:", next_step

Now we’ve given our user endless opportunities to enter a valid choice.

Make a selection: Rad
Rad is not a valid selection!
Make a selection: 42
42 is not a valid selection!
Make a selection: 0
You decided to: quit

What is going on here? We have created a variable next_step and assigned it the value of …. None? We are going to be using next_step in the test condition of our loop, and Python will complain to us if the variable isn’t defined before we start testing its value. Using the None value is more convenient than arbitrarily declaring a particular value to be invalid and using that. None is a special value meaning “nothing at all” - not even the numeric value of zero. Think of the statement next_step = None as our way to tell Python “I plan on using a variable called next_step but I don’t have a value for it yet. Just remember that I told you I wanted the variable.”

Now for the while loop. We specify a condition here, similar to the way we did with if earlier. The condition is that next_step must not be None. It is an easy enough requirement. The test will fail if we successfully assign a next_step in the loop.

Catching specific exceptions

There is one more minor issue to take care of before we wrap up this stage of writing the game. It is good that we are handling exceptions raised from user input, but we are catching every exception that is raised. This doesn’t sound like a bad thing until you remember that our error message is really written for a specific kind of error: the user entered something that can’t be used by our menu handler. There are many things) that can go wrong in a Python code. We don’t want to be handling exceptions that we aren’t ready for. Why not? The error messages won’t make sense, for starters. Say you decided to hit Control-C in the program to force quit. Here’s what we end up seeing:

Make a selection: ^CTraceback (most recent call last):
  File "ifiction.py", line 40, in ?
    print choice, "is not a valid selection!"
NameError: name 'choice' is not defined

WARNING

Please don’t explore this with other types of bad input. You could end up with a Python process that won’t quit unless you force it to quit from your task / process manager.

The problem is that the original exception was a KeyboardInterrupt. We don’t see that here, because we referred to choice which isn’t defined until the user provides some input. This causes a NameError to be raised, which hides the original exception. Python usually tells you only about the most recent exception that happened. If something truly unexpected happens here, we will never know about it. Python normally tells you about the most recent exception only.

We caused a KeyboardInterrupt by hitting Control-C. Python sees our catch-all except block and hands the KeyboardInterrupt exception to that block. Inside the block, we try to include the menu choice in our error message. Unfortunately, choice hasn’t been defined yet. We never entered a choice! This is a whole new exception, and we don’t have any code to handle it. Python’s rule is to always stop and inform you of the first unhandled exception it encounters. and because of the way we defined and wrote our except block, every exception is treated like bad input - from Control-C to a missing hard drive. As far as Python is concerned, we’ve handled the KeyboardInterrupt. The NameError caused by choice in the exception handler is the surprise that makes Python panic.

That is enough of a lecture. I am sure you understand by now that we want to be specific about what exceptions we are ready for. It is time to make it happen. The two exceptions that we care about are:

IndexError
we tried to use a bad number as a list index
ValueError
the user input couldn’t be converted to a number

Let’s add a handler for IndexError.

while next_step == None:
    try:
        choice = raw_input("Make a selection: ")
        menu_selection = int(choice)

        if menu_selection == 0:
            next_step = "quit"
        else:
            index = menu_selection - 1
            next_step = paths[ index ]
            print "Choice", choice, "-", next_step
    except IndexError:
        print choice, "is not a valid selection!"

This change now handles IndexError just fine. Look at what happens when we hit Control-C.

Make a selection: ^CTraceback (most recent call last):
  File "ifiction.py", line 30, in ?
    choice = raw_input("Make a selection: ")
KeyboardInterrupt

Of course, we’re only looking for IndexError, so look what happens right now if we enter something that isn’t a number.

Make a selection: mmm coffee
Traceback (most recent call last):
  File "ifiction.py", line 31, in ?
    menu_selection = int(choice)
ValueError: invalid literal for int(): mmm coffee

Now we add the code to handle ValueError.

except IndexError:
    print choice, "is not a valid selection!"
except ValueError:
    print choice, "is not a valid selection!"

Yes, Python can handle multiple except blocks just fine. This can be very handy. A calculator program would want to handle “no input” differently from “user tried to divide by zero.” What does the program look like with these changes?

Make a selection: 12
12 is not a valid selection!
Make a selection: Can I have a banana?
Can I have a banana? is not a valid selection!
Make a selection: ^CTraceback (most recent call last):
  File "ifiction.py", line 30, in ?
    choice = raw_input("Make a selection: ")
KeyboardInterrupt

Wonderful! Our exception handling code is now behaving politely instead of trying to grab every exception that occurs. Now for the style issue. Both of our except blocks do exactly the same thing. I suppose we could have a slightly different error message for each kind of exception, and you are free to do exactly that. It’s not something I’m concerned about, though. I am comfortable with using the same error message. I would prefer to cut down on the repetition.

except (IndexError, ValueError):
    print choice, "is not a valid selection!"

I would like to look again at this code in the future, but for now it is good enough.

We can handle multiple exceptions in the same except block by placing the exception types in a special list called a tuple. I am not going to spend any time on tuples, because I worry that it would only confuse things. All you need to remember right now is that a tuple looks like an ordinary list using () instead of [] to wrap it, and that you should use normal lists unless I tell you otherwise.

It looks like we have all the code we need for handling a single round in our game. I had to cover more new concepts than I thought, because things can become complicated when we start doing things with user input. We dabbled into importing modules thanks to the way things were printing out in my shell. We looked at the common control structures for selection and repetition, and we examined try for trying out code that we know can misbehave. Here’s the full source of what we’ve done so far, along with a couple of additional comments intended to clarify what the program is doing.

# ifiction.py
#  - An interactive fiction game

import textwrap

description = "You are standing in a field. To the north of you are some mountains, " \
              "to the east of you is a forest, to the west of you is a cave, and to " \
              "the south of you is a valley."

print textwrap.fill(description)

# The list of choices available to the user.
paths = [
    "Go to the mountains",
    "Go into the forest",
    "Go into the cave",
    "Go to the valley"
]

# Show the menu for this scene.
for i in range(0, len(paths)):
    path = paths[i]
    menu_item = i + 1
    print "\t", menu_item, path

print "\t(0 Quit)"
next_step = None

# Get the user selection from the menu.
while next_step == None:
    try:
        choice = raw_input("Make a selection: ")
        menu_selection = int(choice)

        if menu_selection == 0:
            next_step = "quit"
        else:
            index = menu_selection - 1
            next_step = paths[ index ]
            print "Choice", choice, "-", next_step
    except (IndexError, ValueError):
        print choice, "is not a valid selection!"

print "You decided to:", next_step

Now go take a break. I really mean it this time. We have covered a lot, and you need time to process. Listen to some music, have a sandwich, and come back when you’re ready.


Added to vault 2024-01-15. Updated on 2024-01-26