LANG·I Language Chapter 3 of 65

Forks in the road

This chapter is a quest: you write a text game modeled on a computer cave from 1976, and each new piece of the language opens another room in it. By the end, the quest is ready to play.

From zero 50 minutes Python Programming History

Builds on: 02 · Names and values

What you will take away

  • write programs that choose what to do: if, elif, else and nested conditions
  • build compound conditions from and, or and not, and check them with a truth table
  • stay out of the classic traps: = instead of ==, an or missing its second comparison, and checks in the wrong order

The programs from the last chapter can remember and ask, but they always take the same road: line after line, top to bottom, whatever the person answers. The mind reader can’t say, “If you thought of more than fifty, then…” To choose, a program needs forks in the road. Out of forks we’ll build a game, a text quest set in a cave: every new piece of the language will open a room in it, and at the end of the chapter you’ll go down into that cave yourself.

But first, a word about another cave, the one a programmer and caver carried into a computer half a century ago.

A game like this is made of “ifs.” If the player said “north” and this room has a passage to the north, go there. If the player has no lamp and it is dark, give a warning. If the player has a bird in a cage and there is a snake ahead… More on that later. Our cave will be humbler, but it works the same way.

Room one. End of the road

The first room is Crowther’s. The program describes the place and asks whether to go into the building. If the player says “yes”, it describes the inside. If not, it doesn’t.

Run it twice: answer “yes”, then “no”. The word if means what it says. After it comes a condition, an expression that is either true or false. The double == asks “are these equal?” and the answer to that question is a Boolean value, True or False, the type bool from the last chapter. If the condition is true, Python runs the lines indented under if; if it is false, Python jumps over them. The last line isn’t under any if, so it always runs.

What the indents are for

In Chapter 1 an extra indent broke a program, and we promised to explain why indents exist at all. Lines indented by the same amount under if form a block: they run together or not at all. The colon at the end of the line with if shows where the block begins. The first line that starts at the left margin again shows where it ends. By convention, an indent is four spaces.

Many languages put a block in curly braces, and the indents are there only for people; the machine ignores them. Python followed ABC, a teaching language its author Guido van Rossum had worked on before, and made indentation part of the language: since people read a program by its indents anyway, the machine might as well read it the same way. The price is strictness. Try removing the indent from the line with the lamp, or indenting the last line instead.

In the program above, indent the last line, print("The stream babbles…"), by four spaces. What changes?

The line is now in the if block and runs only together with it. The stream has stopped babbling for anyone who walked past the building, though not a single word of the program has changed.

A fork is easy to draw: a diamond for a condition, a rectangle for an action. The diagram below is drawn by the code itself. Press “Walk it,” answer the program’s question, and the path it took lights up.

Change the code and the diagram rebuilds as you type. “Walk it” runs the program on the server.

Room two. Inside the building

There is a lamp in the building. The player either takes it or leaves it, and either way the program should say something. For the second path there is the word else:

One of the two blocks runs, and only one: either the block under if or the block under else. The variable lamp remembers the player’s choice, and the rooms further on can check it. Programmers call yes-or-no variables like this one flags.

There is a shorter way to set a flag. The comparison answer == "yes" is a value in its own right, True or False, and it can be stored straight away:

The player answered “Yes”, with a capital letter. What ends up in lamp?

False. To Python “Yes” and “yes” are different strings, because a capital Y and a small y are different characters. Chapter 7 will teach the program to forgive capitals and stray spaces; until then, answer in lower case.

Room three. The grate

The stream disappears underground, and the way into the cave is barred by a steel grate with a combination lock. The combination is a number. The lock compares it with the right one and tells you which way you are off. Besides ==, Python has five more comparisons:

WrittenRead asTrue, for example
a == bequal to2 + 2 == 4
a != bnot equal to"yes" != "Yes"
a < bless than1023 < 1024
a > bgreater than1025 > 1024
a <= bat most18 <= 18
a >= bat least18 >= 18

The four separate ifs are checked independently, one after another, and with a wrong code two of them fire. Comparisons can be chained, as in mathematics: 1000 <= code <= 9999 means “the code has four digits.” Strings can be compared too, but in their own way.

What does the second line, print("10" < "9"), print?

True. Strings are compared the way a dictionary orders words: by the first character where they differ. That is why “cart” comes before “cat”: the first letters that differ are r and t. The first characters of "10" and "9" are “1” and “9”, and “1” comes earlier in the character table. That is why numbers from input() are turned into int before they are compared.

The most common typo

One equals sign assigns, two compare. They are easy to mix up, especially at first.

Python won’t run this program. Its error message even names both fixes, == and :=, in a polite question: Maybe you meant '==' or ':=' instead of '='? Other languages are less considerate. In C a similar line, if (code = 1024), is not an error: it assigns and then checks the result, and 1024 isn’t zero, so the condition is true. Typos like this survived for years in working programs, so the designers of Python banned a lone = inside a condition outright. Since version 3.8 you can assign inside a condition after all, but only with a separate operator, :=, which nobody will mistake for ==.

Room four. The hall

Beyond the grate lies a huge hall with three passages leading out of it. You could write three separate ifs, but then you would need one more for an answer that fits none of the passages, and its condition would come out long and tangled. For a choice among many options there is elif, short for “else if”:

Python checks the conditions from top to bottom and runs the block of the first true one. It ignores the rest, even if they are true as well. The else at the end catches whatever didn’t fit, and it can be left out. The diagram of such a chain looks like a staircase: every “no” leads to the next question.

An if/elif/else chain. Walk it with different answers, including one the program doesn’t expect.

The order of checks

The rule “the first true one wins” hides a trap. Down in the cave the lamp’s battery is running low, and the program describes the light by the charge left:

Swap the first and third checks: charge > 0 first, then charge > 10, then charge > 50. What does the program say when the charge is 80?

“The lamp flickers.” The first condition, charge > 0, is true for 80, so Python runs its block and looks no further. The “bright” and “dim” branches can no longer be reached: any number that would land in them is caught earlier by the first check.

On the number line below, the color shows which branch catches each value of the charge. Move a broad check to the top, and it swallows the narrower ones beneath it.

The arrows change the order of the checks. Under the line are a slider for the charge and the program’s answer; below them, the code that results.

Hence the rule: in an elif chain, narrow conditions go before broad ones. If one condition is broader than another, the way “more than 10” is broader than “more than 50,” the stricter one goes first. Python won’t catch this mistake for you: the program runs without a single complaint and quietly takes the wrong path.

Room five. The bird and the snake

Crowther and Woods’s cave holds one of the best-known puzzles of early computer games. A little bird lives in one of the rooms. Further on, in the Hall of the Mountain King, a huge snake blocks the way, and only the bird can drive it off. But the bird can be caught only in a cage, and it is afraid of a black rod the player finds nearby: carry the rod, and you can’t get near it. So catching the bird takes two conditions at once: you have the cage and you don’t have the rod.

Swap True and False in the first lines and try every outcome. Three words build compound conditions out of simple ones:

  • a and b: true only when both are true;
  • a or b: true when at least one of them is true;
  • not a: turns true into false and false into true.

However many simple conditions there are, each has only two values, so you can go through every combination of them and write down what the compound condition comes to for each one. Such a table is called a truth table. For two variables it has four rows, for three it has eight, and for $n$ variables $2^n$.

Seen through Shannon’s eyes, the condition for catching the bird is a circuit. Each variable is a switch, and puts switches one after another, or puts them side by side, and not turns a switch into an “inverted” one that is closed when the variable is false. The lamp at the end of the circuit lights up when the whole expression is true.

Type any condition made of names, and, or, not and parentheses. Click the switches or the rows of the table. The circuit is built from the expression itself.

Compare not (cage and rod) and not cage or not rod in the widget: their tables match row for row. This is one of De Morgan’s laws: “it is not the case that both hold” means the same as “the first fails or the second fails.” Laws like this help untangle knotted conditions, and in Chapter 29, circuits.

The or trap

Players are lazy and type “y” instead of “yes”. It is tempting to write the condition the way you would say it aloud: “if the answer is yes or y.”

The player answers “no”. What does the program print?

“You enter the building,” whatever the answer. Python doesn’t read the condition as “the answer equals yes or y.” It reads (answer == "yes") or ("y"). The left half is false, and the right half is the non-empty string "y", which counts as true. The correct version is answer == "yes" or answer == "y", with a comparison on each side.

Why the string "y" can stand in a condition at all is a question for the next room.

Room six. The nameless traveler

A condition doesn’t have to be a comparison: any value will do. Python decides whether a value is true by one rule: empty and zero are false, everything else is true. The false values are False, 0, 0.0, the empty string "", the empty list and a special value, None, meaning “nothing.” You have met this before, at the type customs in the last chapter: bool("") is False, while bool("0") is True.

Press Enter without typing anything: input() returns an empty string, and the else branch runs. if name: is shorter than if name != "":, and it is how Python programmers write it. And not turns the check around: if not name: means “if there is no name.”

Room seven. The maze

The most famous place in the old cave is the maze, which the game described like this: you are in a maze of twisty little passages, all alike. Our maze has two forks in a row, and you reach the second one only if you turn left at the first. A block inside a block is a nested condition:

Each level of nesting adds four more spaces. The indents show which else belongs to which if: the lower else lines up with the first if and answers to it. On the diagram a nested condition is a diamond inside a branch of another diamond.

Nested conditions: the second fork can be reached only along the “yes” branch of the first.

Nesting easily grows into a staircase that runs off the right edge of the screen, and five levels of indentation are already hard going. Often you can flatten it by joining the conditions with and:

This version reads from top to bottom without a staircase, but it asks about the second fork even when the player turned right at the first. Which is better depends on the game. A good rule of thumb: nest a condition when the second question makes sense only after the answer to the first.

A sign at the entrance: match

Text games understand commands: “north”, “south”, “take lamp”. A long elif way == … chain for every command gets tedious, and since 2021 (version 3.10) Python has had the match statement: a value is checked against patterns, one by one.

The vertical bar | means “or,” and the underscore _ catches everything else, like else. The first pattern that fits wins. The magic word XYZZY comes from Adventure: it carried the player from the building to a room underground, past the locked grate, and back again, and ever since, programmers have hidden it in their software as an in-joke. match can do far more than pick among strings (it can take complicated values apart), but this much is enough for our chapter.

The whole cave

Time to put the rooms together into one game. Below is the full quest: the road, the building with a lamp, a cage and a rod, the grate with its code (the hint is scratched on the building’s wall), the hall, the bird and the snake, the maze. The flag alive remembers whether the player is still alive: if they have died or are stuck at a locked grate, the rooms that follow check the flag and are skipped. The line lamp = cage = rod = bird = False ties four names to one object, False. That is safe: a Boolean value can’t be changed; you can only move a label to another object (remember the eighth trick?).

The game is easier to play in the terminal below: it runs the code from the cell above, with all your changes, and marks the rooms you have visited on the map. Try to find both treasures (you can’t reach both in one game; it takes two), and then add a room of your own. If the program prints a line like print("== Name ==") for your room, the room appears under the map, in the list of your rooms.

The cave terminal. The program runs on the server and waits for your answers, while the map watches for the == Room == lines the game prints.
How many paths this cave has

Every if splits the path in two, and the number of possible playthroughs grows fast. The building alone asks three independent yes-or-no questions, which gives $2^3 = 8$ combinations of items. Multiply by the outcomes at the grate, the choice in the hall and the two forks of the maze, and you get several dozen different stories. Playing them all through by hand is hard work, and Chapter 11 teaches you to write tests that check the branches and their boundaries for you. The troll bridge task below is a first step: there the server checks every branch of your room.

Tasks

Four tasks on sorting out cases. In each one the program reads answers with input(), and the server checks it on every branch, including the boundary values where the conditions switch over. Boundary values are where testing any program with conditions should begin.

Under the Gregorian calendar, introduced in 1582, a year is a leap year if it is divisible by 4, except for years divisible by 100: those are leap years only if they are also divisible by 400. So 1900 was a common year, and 2000 a leap year. The program reads a year and prints one word: leap or common.

The starter is right for 2024 and 2023. Try 1900: it is divisible by 4, yet it was a common year.

You have known the remainder operator, %, since Chapter 1: year % 100 == 0 means “divisible by 100.”

The order of checks: the strictest rule first (divisible by 400), then the exception (divisible by 100), then the general rule (divisible by 4).

The narrow conditions come before the broad ones, as in the lamp-charge check. The same thing in one expression: year % 4 == 0 and (year % 100 != 0 or year % 400 == 0). The rule about centuries is there because a year lasts about 365.2422 days rather than 365.25: without it the calendar would drift away from the sun by a day every 128 years.

The program reads three whole numbers, the lengths of the sides, one per line, and prints one of four things: equilateral, isosceles, scalene or not a triangle. Three segments make a triangle only if each is shorter than the sum of the other two (if one equals the sum, the segments lie flat along one line and there is no triangle). Segments of zero or negative length don’t make a triangle either.

First weed out whatever isn’t a triangle at all: a + b <= c or … (add the checks for the other two sides). This check also weeds out negative and zero lengths on its own; think about why.

Isosceles means that at least two sides are equal: a == b or b == c or a == c. An equilateral triangle fits that too, so it is checked first.

Zero and negative lengths need no separate check: if all three triangle inequalities hold, every side is positive. Add a + b > c and a + c > b, and you get 2a + b + c > b + c, that is, a > 0; the same goes for b and c. And once again the order of the branches decides: an equilateral triangle is isosceles as well, so it is checked first.

Two players take turns entering their moves: rock, scissors or paper. Rock blunts scissors, scissors cut paper, paper covers rock. The program prints first, second or draw.

The draw is already there. When does the first player win? Write out the three cases and join them with or.

Each case is a pair of conditions joined with and: first == "rock" and second == "scissors". If it is not a draw and the first player didn’t win, the second one did.

and binds more tightly than or, so each pair is put together first, the way multiplication comes before addition. The parentheses around the whole condition are there only so that it can be split across several lines. With the dictionaries of Chapter 8 the same rule can be written more briefly, as a dictionary of who beats whom.

A new room for the quest. The program asks how many coins the player has (a whole number) and whether they have a sword (the answer is yes or no), and prints one of these phrases as its last line:

  • 5 coins or more: The troll takes five coins and lets you pass.
  • 1 to 4 coins and a sword: The troll eyes your sword and takes what you have.
  • no coins, but a sword: A fight! The troll runs off under the bridge.
  • in every other case: The troll won't let you onto the bridge.

The server will check every branch and every boundary: 5 coins, 4, 1, 0.

Start with the simplest case, five coins or more. The sword doesn’t matter there.

“From 1 to 4” is 1 <= coins <= 4, but if five or more have already been dealt with above, coins >= 1 is enough. “No coins” is coins == 0.

The server’s tests are laid out as a table of every path: each combination of “how many coins” and “is there a sword” at the boundaries of the conditions. Add the room to your quest: a bridge fits nicely between the hall and the maze.

What next

Our quest can choose, but it asks each question only once. Get the lock code wrong, and the game is over. To ask again, you would have to copy a piece of the program, and to keep asking until the player gets it right, copy it without end. You can’t go back from the hall to the road or wander around the maze. A computer performs on the order of a billion operations a second, and we are writing each one out as a separate line. How do you make it repeat an action ten times, a thousand, a million, or until something happens? That’s Chapter 4.