Bagels is a deduction game in which the player tries to guess a random three-digit number (with no repeating digits) generated by the computer. After each guess, the computer gives the player three types of clues:
Bagels None of the three digits guessed is in the secret number.
Pico One of the digits is in the secret number, but the guess has the digit in the wrong place.
Fermi The guess has a correct digit in the correct place.
The computer can give multiple clues, which are sorted in alphabetical order. If the secret number is 456 and the player’s guess is 546, the clues would be “fermi pico pico.” The “fermi” is from the 6 and “pico pico” are from the 4 and 5.
In this chapter, you’ll learn a few new methods and functions that come with Python. You’ll also learn about augmented assignment operators and string interpolation. While they don’t let you do anything you couldn’t do before, they are nice shortcuts to make coding easier.
Here’s what the user sees when they run the Bagels program. The text the player enters is shown in bold.
I am thinking of a 3-digit number. Try to guess what it is.
The clues I give are...
When I say: That means:
Bagels None of the digits is correct.
Pico One digit is correct but in the wrong position.
Fermi One digit is correct and in the right position.
I have thought up a number. You have 10 guesses to get it.
Guess #1:
123
Fermi
Guess #2:
453
Pico
Guess #3:
425
Fermi
Guess #4:
326
Bagels
Guess #5:
489
Bagels
Guess #6:
075
Fermi Fermi
Guess #7:
015
Fermi Pico
Guess #8:
175
You got it!
Do you want to play again? (yes or no)
no
In a new file, enter the following source code and save it as bagels.py. Then run the game by pressing F5. If you get errors, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
bagels.py
1. import random
2.
3. NUM_DIGITS = 3
4. MAX_GUESS = 10
5.
6. def getSecretNum():
7. # Returns a string of unique random digits that is NUM_DIGITS long.
8. numbers = list(range(10))
9. random.shuffle(numbers)
10. secretNum = ''
11. for i in range(NUM_DIGITS):
12. secretNum += str(numbers[i])
13. return secretNum
14.
15. def getClues(guess, secretNum):
16. # Returns a string with the Pico, Fermi, & Bagels clues to the user.
17. if guess == secretNum:
18. return 'You got it!'
19.
20. clues = []
21. for i in range(len(guess)):
22. if guess[i] == secretNum[i]:
23. clues.append('Fermi')
24. elif guess[i] in secretNum:
25. clues.append('Pico')
26. if len(clues) == 0:
27. return 'Bagels'
28.
29. clues.sort()
30. return ' '.join(clues)
31.
32. def isOnlyDigits(num):
33. # Returns True if num is a string of only digits. Otherwise, returns
False.
34. if num == '':
35. return False
36.
37. for i in num:
38. if i not in '0 1 2 3 4 5 6 7 8 9'.split():
39. return False
40.
41. return True
42.
43.
44. print('I am thinking of a %s-digit number. Try to guess what it is.' %
(NUM_DIGITS))
45. print('The clues I give are...')
46. print('When I say: That means:')
47. print(' Bagels None of the digits is correct.')
48. print(' Pico One digit is correct but in the wrong position.')
49. print(' Fermi One digit is correct and in the right position.')
50.
51. while True:
52. secretNum = getSecretNum()
53. print('I have thought up a number. You have %s guesses to get it.' %
(MAX_GUESS))
54.
55. guessesTaken = 1
56. while guessesTaken <= MAX_GUESS:
57. guess = ''
58. while len(guess) != NUM_DIGITS or not isOnlyDigits(guess):
59. print('Guess #%s: ' % (guessesTaken))
60. guess = input()
61.
62. print(getClues(guess, secretNum))
63. guessesTaken += 1
64.
65. if guess == secretNum:
66. break
67. if guessesTaken > MAX_GUESS:
68. print('You ran out of guesses. The answer was %s.' %
(secretNum))
69.
70. print('Do you want to play again? (yes or no)')
71. if not input().lower().startswith('y'):
72. break
The flowchart in Figure 11-1 describes what happens in this game and the order in which each step can happen.
The flowchart for Bagels is pretty simple. The computer generates a secret number, the player tries to guess that number, and the computer gives the player clues based on their guess. This happens over and over again until the player either wins or loses. After the game finishes, whether the player won or not, the computer asks the player whether they want to play again.
Figure 11-1: Flowchart for the Bagels game
At the start of the program, we’ll import the random module and set up some global variables. Then we’ll define a function named getSecretNum().
1. import random
2.
3. NUM_DIGITS = 3
4. MAX_GUESS = 10
5.
6. def getSecretNum():
7. # Returns a string of unique random digits that is NUM_DIGITS long.
Instead of using the integer 3 for the number of digits in the answer, we use the constant variable NUM_DIGITS. The same goes for the number of guesses the player gets; we use the constant variable MAX_GUESS instead of the integer 10. Now it will be easy to change the number of guesses or secret number digits. Just change the values at line 3 or 4, and the rest of the program will still work without any more changes.
The getSecretNum() function generates a secret number that contains only unique digits. The Bagels game is much more fun if you don’t have duplicate digits in the secret number, such as '244' or '333'. We’ll use some new Python functions to make this happen in getSecretNum().
The first two lines of getSecretNum() shuffle a set of nonrepeating numbers:
8. numbers = list(range(10))
9. random.shuffle(numbers)
Line 8’s list(range(10)) evaluates to [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], so the numbers variable contains a list of all 10 digits.
The random.shuffle() function randomly changes the order of a list’s items (in this case, the list of digits). This function doesn’t return a value but rather modifies the list you pass it in place. This is similar to the way the makeMove() function in Chapter 10’s Tic-Tac-Toe game modified the list it was passed in place, rather than returning a new list with the change. This is why you do not write code like numbers = random.shuffle(numbers).
Try experimenting with the shuffle() function by entering the following code into the interactive shell:
>>> import random
>>> spam = list(range(10))
>>> print(spam)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> random.shuffle(spam)
>>> print(spam)
[3, 0, 5, 9, 6, 8, 2, 4, 1, 7]
>>> random.shuffle(spam)
>>> print(spam)
[9, 8, 3, 5, 4, 7, 1, 2, 0, 6]
Each time random.shuffle() is called on spam, the items in the spam list are shuffled. You’ll see how we use the shuffle() function to make a secret number next.
The secret number will be a string of the first NUM_DIGITS digits of the shuffled list of integers:
10. secretNum = ''
11. for i in range(NUM_DIGITS):
12. secretNum += str(numbers[i])
13. return secretNum
The secretNum variable starts out as a blank string. The for loop on line 11 iterates NUM_DIGITS number of times. On each iteration through the loop, the integer at index i is pulled from the shuffled list, converted to a string, and concatenated to the end of secretNum.
For example, if numbers refers to the list [9, 8, 3, 5, 4, 7, 1, 2, 0, 6], then on the first iteration, numbers[0] (that is, 9) will be passed to str(); this returns '9', which is concatenated to the end of secretNum. On the second iteration, the same happens with numbers[1] (that is, 8), and on the third iteration the same happens with numbers[2] (that is, 3). The final value of secretNum that is returned is '983'.
Notice that secretNum in this function contains a string, not an integer. This may seem odd, but remember that you cannot concatenate integers. The expression 9 + 8 + 3 evaluates to 20, but what you want is '9' + '8' + '3', which evaluates to '983'.
The += operator on line 12 is one of the augmented assignment operators. Normally, if you want to add or concatenate a value to a variable, you use code that looks like this:
>>> spam = 42
>>> spam = spam + 10
>>> spam
52
>>> eggs = 'Hello '
>>> eggs = eggs + 'world!'
>>> eggs
'Hello world!'
The augmented assignment operators are shortcuts that free you from retyping the variable name. The following code does the same thing as the previous code:
>>> spam = 42
>>> spam += 10 # The same as spam = spam + 10
>>> spam
52
>>> eggs = 'Hello '
>>> eggs += 'world!' # The same as eggs = eggs + 'world!'
>>> eggs
'Hello world!'
There are other augmented assignment operators as well. Enter the following into the interactive shell:
>>> spam = 42
>>> spam -= 2
>>> spam
40
The statement spam –= 2 is the same as the statement spam = spam – 2, so the expression evaluates to spam = 42 – 2 and then to spam = 40.
There are augmented assignment operators for multiplication and division, too:
>>> spam *= 3
>>> spam
120
>>> spam /= 10
>>> spam
12.0
The statement spam *= 3 is the same as spam = spam * 3. So, since spam was set equal to 40 earlier, the full expression would be spam = 40 * 3, which evaluates to 120. The expression spam /= 10 is the same as spam = spam / 10, and spam = 120 / 10 evaluates to 12.0. Notice that spam becomes a floating point number after it’s divided.
The getClues() function will return a string with fermi, pico, and bagels clues depending on the guess and secretNum parameters.
15. def getClues(guess, secretNum):
16. # Returns a string with the Pico, Fermi, & Bagels clues to the user.
17. if guess == secretNum:
18. return 'You got it!'
19.
20. clues = []
21. for i in range(len(guess)):
22. if guess[i] == secretNum[i]:
23. clues.append('Fermi')
24. elif guess[i] in secretNum:
25. clues.append('Pico')
The most obvious step is to check whether the guess is the same as the secret number, which we do in line 17. In that case, line 18 returns 'You got it!'.
If the guess isn’t the same as the secret number, the program must figure out what clues to give the player. The list in clues will start empty and have 'Fermi' and 'Pico' strings added as needed.
The program does this by looping through each possible index in guess and secretNum. The strings in both variables will be the same length, so line 21 could have used either len(guess) or len(secretNum) and worked the same. As the value of i changes from 0 to 1 to 2, and so on, line 22 checks whether the first, second, third, and so on character of guess is the same as the character in the corresponding index of secretNum. If so, line 23 adds the string 'Fermi' to clues.
Otherwise, line 24 checks whether the number at the ith position in guess exists anywhere in secretNum. If so, you know that the number is somewhere in the secret number but not in the same position. In that case, line 25 then adds 'Pico' to clues.
If the clues list is empty after the loop, then you know that there are no correct digits at all in guess:
26. if len(clues) == 0:
27. return 'Bagels'
In this case, line 27 returns the string 'Bagels' as the only clue.
Lists have a method named sort() that arranges the list items in alphabetical or numerical order. When the sort() method is called, it doesn’t return a sorted list but rather sorts the list in place. This is just like how the shuffle() method works.
You would never want to use return spam.sort() because that would return the value None. Instead you want a separate line, spam.sort(), and then the line return spam.
Enter the following into the interactive shell:
>>> spam = ['cat', 'dog', 'bat', 'anteater']
>>> spam.sort()
>>> spam
['anteater', 'bat', 'cat', 'dog']
>>> spam = [9, 8, 3, 5.5, 5, 7, 1, 2.1, 0, 6]
>>> spam.sort()
>>> spam
[0, 1, 2.1, 3, 5, 5.5, 6, 7, 8, 9]
When we sort a list of strings, the strings are returned in alphabetical order, but when we sort a list of numbers, the numbers are returned in numerical order.
On line 29, we use sort() on clues:
29. clues.sort()
The reason you want to sort the clue list alphabetically is to get rid of extra information that would help the player guess the secret number more easily. If clues was ['Pico', 'Fermi', 'Pico'], that would tell the player that the center digit of the guess is in the correct position. Since the other two clues are both Pico, the player would know that all they have to do to get the secret number is swap the first and third digits.
If the clues are always sorted in alphabetical order, the player can’t be sure which number the Fermi clue refers to. This makes the game harder and more fun to play.
The join() string method returns a list of strings as a single string joined together.
30. return ' '.join(clues)
The string that the method is called on (on line 30, this is a single space, ' ') appears between each string in the list. To see an example, enter the following into the interactive shell:
>>> ' '.join(['My', 'name', 'is', 'Zophie'])
'My name is Zophie'
>>> ', '.join(['Life', 'the Universe', 'and Everything'])
'Life, the Universe, and Everything'
So the string that is returned on line 30 is each string in clue combined with a single space between each string. The join() string method is sort of like the opposite of the split() string method. While split() returns a list from a split-up string, join() returns a string from a combined list.
The isOnlyDigits() function helps determine whether the player entered a valid guess:
32. def isOnlyDigits(num):
33. # Returns True if num is a string of only digits. Otherwise, returns
False.
34. if num == '':
35. return False
Line 34 first checks whether num is the blank string and, if so, returns False.
The for loop then iterates over each character in the string num:
37. for i in num:
38. if i not in '0 1 2 3 4 5 6 7 8 9'.split():
39. return False
40.
41. return True
The value of i will have a single character on each iteration. Inside the for block, the code checks whether i exists in the list returned by '0 1 2 3 4 5 6 7 8 9'.split(). (The return value from split() is equivalent to ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].) If i doesn’t exist in that list, you know there’s a nondigit character in num. In that case, line 39 returns False.
But if the execution continues past the for loop, then you know that every character in num is a digit. In that case, line 41 returns True.
After all of the function definitions, line 44 is the actual start of the program:
44. print('I am thinking of a %s-digit number. Try to guess what it is.' %
(NUM_DIGITS))
45. print('The clues I give are...')
46. print('When I say: That means:')
47. print(' Bagels None of the digits is correct.')
48. print(' Pico One digit is correct but in the wrong position.')
49. print(' Fermi One digit is correct and in the right position.')
The print() function calls tell the player the rules of the game and what the pico, fermi, and bagels clues mean. Line 44’s print() call has % (NUM_DIGITS) added to the end and %s inside the string. This is a technique known as string interpolation.
String interpolation, also known as string formatting, is a coding shortcut. Normally, if you want to use the string values inside variables in another string, you have to use the + concatenation operator:
>>> name = 'Alice'
>>> event = 'party'
>>> location = 'the pool'
>>> day = 'Saturday'
>>> time = '6:00pm'
>>> print('Hello, ' + name + '. Will you go to the ' + event + ' at ' +
location + ' this ' + day + ' at ' + time + '?')
Hello, Alice. Will you go to the party at the pool this Saturday at 6:00pm?
As you can see, it can be time-consuming to type a line that concatenates several strings. Instead, you can use string interpolation, which lets you put placeholders like %s into the string. These placeholders are called conversion specifiers. Once you’ve put in the conversion specifiers, you can put all the variable names at the end of the string. Each %s is replaced with a variable at the end of the line, in the order in which you entered the variable. For example, the following code does the same thing as the previous code:
>>> name = 'Alice'
>>> event = 'party'
>>> location = 'the pool'
>>> day = 'Saturday'
>>> time = '6:00pm'
>>> print('Hello, %s. Will you go to the %s at %s this %s at %s?' % (name,
event, location, day, time))
Hello, Alice. Will you go to the party at the pool this Saturday at 6:00pm?
Notice that the first variable name is used for the first %s, the second variable for the second %s, and so on. You must have the same number of %s conversion specifiers as you have variables.
Another benefit of using string interpolation instead of string concatenation is that interpolation works with any data type, not just strings. All values are automatically converted to the string data type. If you concatenated an integer to a string, you’d get this error:
>>> spam = 42
>>> print('Spam == ' + spam)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
String concatenation can only combine two strings, but spam is an integer. You would have to remember to put str(spam) instead of spam.
Now enter this into the interactive shell:
>>> spam = 42
>>> print('Spam is %s' % (spam))
Spam is 42
With string interpolation, this conversion to strings is done for you.
Line 51 is an infinite while loop that has a condition of True, so it will loop forever until a break statement is executed:
51. while True:
52. secretNum = getSecretNum()
53. print('I have thought up a number. You have %s guesses to get it.' %
(MAX_GUESS))
54.
55. guessesTaken = 1
56. while guessesTaken <= MAX_GUESS:
Inside the infinite loop, you get a secret number from the getSecretNum() function. This secret number is assigned to secretNum. Remember, the value in secretNum is a string, not an integer.
Line 53 tells the player how many digits are in the secret number by using string interpolation instead of string concatenation. Line 55 sets the variable guessesTaken to 1 to mark this is as the first guess. Then line 56 has a new while loop that loops as long as the player has guesses left. In code, this is when guessesTaken is less than or equal to MAX_GUESS.
Notice that the while loop on line 56 is inside another while loop that started on line 51. These loops inside loops are called nested loops. Any break or continue statements, such as the break statement on line 66, will only break or continue out of the innermost loop, not any of the outer loops.
The guess variable holds the player’s guess returned from input(). The code keeps looping and asking the player for a guess until they enter a valid guess:
57. guess = ''
58. while len(guess) != NUM_DIGITS or not isOnlyDigits(guess):
59. print('Guess #%s: ' % (guessesTaken))
60. guess = input()
A valid guess has only digits and the same number of digits as the secret number. The while loop that starts on line 58 checks for the validity of the guess.
The guess variable is set to the blank string on line 57, so the while loop’s condition on line 58 is False the first time it is checked, ensuring the execution enters the loop starting on line 59.
After execution gets past the while loop that started on line 58, guess contains a valid guess. Now the program passes guess and secretNum to the getClues() function:
62. print(getClues(guess, secretNum))
63. guessesTaken += 1
It returns a string of the clues, which are displayed to the player on line 62. Line 63 increments guessesTaken using the augmented assignment operator for addition.
Now we figure out if the player won or lost the game:
65. if guess == secretNum:
66. break
67. if guessesTaken > MAX_GUESS:
68. print('You ran out of guesses. The answer was %s.' %
(secretNum))
If guess is the same value as secretNum, the player has correctly guessed the secret number, and line 66 breaks out of the while loop that was started on line 56. If not, then execution continues to line 67, where the program checks whether the player ran out of guesses.
If the player still has more guesses, execution jumps back to the while loop on line 56, where it lets the player have another guess. If the player runs out of guesses (or the program breaks out of the loop with the break statement on line 66), execution proceeds past the loop and to line 70.
Line 70 asks the player whether they want to play again:
70. print('Do you want to play again? (yes or no)')
71. if not input().lower().startswith('y'):
72. break
The player’s response is returned by input(), has the lower() method called on it, and then the startswith() method called on that to check if the player’s response begins with a y. If it doesn’t, the program breaks out of the while loop that started on line 51. Since there’s no more code after this loop, the program terminates.
If the response does begin with y, the program does not execute the break statement and execution jumps back to line 51. The program then generates a new secret number so the player can play a new game.
Bagels is a simple game to program but can be difficult to win. But if you keep playing, you’ll eventually discover better ways to guess using the clues the game gives you. This is much like how you’ll get better at programming the more you keep at it.
This chapter introduced a few new functions and methods—shuffle(), sort(), and join()—along with a couple of handy shortcuts. Augmented assignment operators require less typing when you want to change a variable’s relative value; for example, spam = spam + 1 can be shortened to spam += 1. With string interpolation, you can make your code much more readable by placing %s (called a conversion specifier) inside the string instead of using many string concatenation operations.
In Chapter 12, we won’t be doing any programming, but the concepts—Cartesian coordinates and negative numbers—will be necessary for the games in the later chapters of the book. These math concepts are used not only in the Sonar Treasure Hunt, Reversegam, and Dodger games we will be making but also in many other games. Even if you already know about these concepts, give Chapter 12 a brief read to refresh yourself.