The Sonar Treasure Hunt game in this chapter is the first to make use of the Cartesian coordinate system that you learned about in Chapter 12. This game also uses data structures, which is just a fancy way of saying it has list values that contain other lists and similar complex variables. As the games you program become more complicated, you’ll need to organize your data into data structures.
In this chapter’s game, the player drops sonar devices at various places in the ocean to locate sunken treasure chests. Sonar is a technology that ships use to locate objects under the sea. The sonar devices in this game tell the player how far away the closest treasure chest is, but not in what direction. But by placing multiple sonar devices, the player can figure out the location of the treasure chest.
There are 3 chests to collect, and the player has only 20 sonar devices to use to find them. Imagine that you couldn’t see the treasure chest in Figure 13-1. Because each sonar device can find only the distance from the chest, not the chest’s direction, the treasure could be anywhere on the ring around the sonar device.
Figure 13-1: The sonar device’s ring touches the (hidden) treasure chest.
But multiple sonar devices working together can narrow down the chest’s location to the exact coordinates where the rings intersect (see Figure 13-2).
Figure 13-2: Combining multiple rings shows where treasure chests could be hidden.
Here’s what the user sees when they run the Sonar Treasure Hunt program. The text the player enters is shown in bold.
S O N A R !
Would you like to view the instructions? (yes/no)
no
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~`~``~``~``~`~`~```~`~``````~```~`~~~`~~```~~`~~~~~`~~`~~~~` 0
1 ~~~~~`~~~~~````~``~~```~``~`~`~`~``~~```~~~`~`~```~~~~`~`~~` 1
2 ```~````~``~`~`~~~``~~`````~~``~``~``~~```~~``~~`~~~````~~`~ 2
3 `````~~``````~`~~~~~```~~``~~~`~`~~~~~~`````~`~```~~~``~``~` 3
4 ~~~`~~~`~`~~~``~~~`~`~``~~~~``~~~~``~~~~`~`~``~~```~``~~`~`~ 4
5 `~``~````~`~`~~``~~~~``````~```~~~~````````~``~~~`~~``~~```` 5
6 ~`~```~~`~~```~````````~~```~```~~~~``~~~`~`~~`~``~~~`~~`~`` 6
7 ~`~~~```~``~```~`~```~~~~~~~`~~`~`~~~~``~```~~~`~```~``~``~` 7
8 `~``~~`~`~~`~~`~~``~```~````~`~```~``~````~~~````~~``~~``~~` 8
9 ~`~``~~````~~```~`~~```~~`~``~`~~``~`~`~~~~`~`~~`~`~```~~``` 9
10 `~~~~~~`~``~``~~~``~``~~~~`~``~```~`~~``~~~~~~``````~~`~``~~ 10
11 ~``~~~````~`~~`~~~`~~~``~``````~`~``~~~~`````~~~``````~`~`~~ 11
12 ~~~~~``~`~````~```~`~`~`~~`~~`~``~~~~~~~`~~```~~``~~`~~~~``` 12
13 `~~```~~````````~~~`~~~```~~~~~~~~`~~``~~`~```~`~~````~~~``~ 13
14 ```~``~`~`~``~```~`~``~`~``~~```~`~~~``~~``~```~`~~`~``````~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
You have 20 sonar device(s) left. 3 treasure chest(s) remaining.
Where do you want to drop the next sonar device? (0-59 0-14) (or type quit)
25 5
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~`~``~``~``~`~`~```~`~``````~```~`~~~`~~```~~`~~~~~`~~`~~~~` 0
1 ~~~~~`~~~~~````~``~~```~``~`~`~`~``~~```~~~`~`~```~~~~`~`~~` 1
2 ```~````~``~`~`~~~``~~`````~~``~``~``~~```~~``~~`~~~````~~`~ 2
3 `````~~``````~`~~~~~```~~``~~~`~`~~~~~~`````~`~```~~~``~``~` 3
4 ~~~`~~~`~`~~~``~~~`~`~``~~~~``~~~~``~~~~`~`~``~~```~``~~`~`~ 4
5 `~``~````~`~`~~``~~~~````5`~```~~~~````````~``~~~`~~``~~```` 5
6 ~`~```~~`~~```~````````~~```~```~~~~``~~~`~`~~`~``~~~`~~`~`` 6
7 ~`~~~```~``~```~`~```~~~~~~~`~~`~`~~~~``~```~~~`~```~``~``~` 7
8 `~``~~`~`~~`~~`~~``~```~````~`~```~``~````~~~````~~``~~``~~` 8
9 ~`~``~~````~~```~`~~```~~`~``~`~~``~`~`~~~~`~`~~`~`~```~~``` 9
10 `~~~~~~`~``~``~~~``~``~~~~`~``~```~`~~``~~~~~~``````~~`~``~~ 10
11 ~``~~~````~`~~`~~~`~~~``~``````~`~``~~~~`````~~~``````~`~`~~ 11
12 ~~~~~``~`~````~```~`~`~`~~`~~`~``~~~~~~~`~~```~~``~~`~~~~``` 12
13 `~~```~~````````~~~`~~~```~~~~~~~~`~~``~~`~```~`~~````~~~``~ 13
14 ```~``~`~`~``~```~`~``~`~``~~```~`~~~``~~``~```~`~~`~``````~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
Treasure detected at a distance of 5 from the sonar device.
You have 19 sonar device(s) left. 3 treasure chest(s) remaining.
Where do you want to drop the next sonar device? (0-59 0-14) (or type quit)
30 5
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~`~``~``~``~`~`~```~`~``````~```~`~~~`~~```~~`~~~~~`~~`~~~~` 0
1 ~~~~~`~~~~~````~``~~```~``~`~`~`~``~~```~~~`~`~```~~~~`~`~~` 1
2 ```~````~``~`~`~~~``~~`````~~``~``~``~~```~~``~~`~~~````~~`~ 2
3 `````~~``````~`~~~~~```~~``~~~`~`~~~~~~`````~`~```~~~``~``~` 3
4 ~~~`~~~`~`~~~``~~~`~`~``~~~~``~~~~``~~~~`~`~``~~```~``~~`~`~ 4
5 `~``~````~`~`~~``~~~~````5`~``3~~~~````````~``~~~`~~``~~```` 5
6 ~`~```~~`~~```~````````~~```~```~~~~``~~~`~`~~`~``~~~`~~`~`` 6
7 ~`~~~```~``~```~`~```~~~~~~~`~~`~`~~~~``~```~~~`~```~``~``~` 7
8 `~``~~`~`~~`~~`~~``~```~````~`~```~``~````~~~````~~``~~``~~` 8
9 ~`~``~~````~~```~`~~```~~`~``~`~~``~`~`~~~~`~`~~`~`~```~~``` 9
10 `~~~~~~`~``~``~~~``~``~~~~`~``~```~`~~``~~~~~~``````~~`~``~~ 10
11 ~``~~~````~`~~`~~~`~~~``~``````~`~``~~~~`````~~~``````~`~`~~ 11
12 ~~~~~``~`~````~```~`~`~`~~`~~`~``~~~~~~~`~~```~~``~~`~~~~``` 12
13 `~~```~~````````~~~`~~~```~~~~~~~~`~~``~~`~```~`~~````~~~``~ 13
14 ```~``~`~`~``~```~`~``~`~``~~```~`~~~``~~``~```~`~~`~``````~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
Treasure detected at a distance of 3 from the sonar device.
You have 18 sonar device(s) left. 3 treasure chest(s) remaining.
Where do you want to drop the next sonar device? (0-59 0-14) (or type quit)
25 10
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~`~``~``~``~`~`~```~`~``````~```~`~~~`~~```~~`~~~~~`~~`~~~~` 0
1 ~~~~~`~~~~~````~``~~```~``~`~`~`~``~~```~~~`~`~```~~~~`~`~~` 1
2 ```~````~``~`~`~~~``~~`````~~``~``~``~~```~~``~~`~~~````~~`~ 2
3 `````~~``````~`~~~~~```~~``~~~`~`~~~~~~`````~`~```~~~``~``~` 3
4 ~~~`~~~`~`~~~``~~~`~`~``~~~~``~~~~``~~~~`~`~``~~```~``~~`~`~ 4
5 `~``~````~`~`~~``~~~~````5`~``3~~~~````````~``~~~`~~``~~```` 5
6 ~`~```~~`~~```~````````~~```~```~~~~``~~~`~`~~`~``~~~`~~`~`` 6
7 ~`~~~```~``~```~`~```~~~~~~~`~~`~`~~~~``~```~~~`~```~``~``~` 7
8 `~``~~`~`~~`~~`~~``~```~````~`~```~``~````~~~````~~``~~``~~` 8
9 ~`~``~~````~~```~`~~```~~`~``~`~~``~`~`~~~~`~`~~`~`~```~~``` 9
10 `~~~~~~`~``~``~~~``~``~~~4`~``~```~`~~``~~~~~~``````~~`~``~~ 10
11 ~``~~~````~`~~`~~~`~~~``~``````~`~``~~~~`````~~~``````~`~`~~ 11
12 ~~~~~``~`~````~```~`~`~`~~`~~`~``~~~~~~~`~~```~~``~~`~~~~``` 12
13 `~~```~~````````~~~`~~~```~~~~~~~~`~~``~~`~```~`~~````~~~``~ 13
14 ```~``~`~`~``~```~`~``~`~``~~```~`~~~``~~``~```~`~~`~``````~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
Treasure detected at a distance of 4 from the sonar device.
You have 17 sonar device(s) left. 3 treasure chest(s) remaining.
Where do you want to drop the next sonar device? (0-59 0-14) (or type quit)
29 8
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~`~``~``~``~`~`~```~`~``````~```~`~~~`~~```~~`~~~~~`~~`~~~~` 0
1 ~~~~~`~~~~~````~``~~```~``~`~`~`~``~~```~~~`~`~```~~~~`~`~~` 1
2 ```~````~``~`~`~~~``~~`````~~``~``~``~~```~~``~~`~~~````~~`~ 2
3 `````~~``````~`~~~~~```~~``~~~`~`~~~~~~`````~`~```~~~``~``~` 3
4 ~~~`~~~`~`~~~``~~~`~`~``~~~~``~~~~``~~~~`~`~``~~```~``~~`~`~ 4
5 `~``~````~`~`~~``~~~~````X`~``X~~~~````````~``~~~`~~``~~```` 5
6 ~`~```~~`~~```~````````~~```~```~~~~``~~~`~`~~`~``~~~`~~`~`` 6
7 ~`~~~```~``~```~`~```~~~~~~~`~~`~`~~~~``~```~~~`~```~``~``~` 7
8 `~``~~`~`~~`~~`~~``~```~````~X~```~``~````~~~````~~``~~``~~` 8
9 ~`~``~~````~~```~`~~```~~`~``~`~~``~`~`~~~~`~`~~`~`~```~~``` 9
10 `~~~~~~`~``~``~~~``~``~~~X`~``~```~`~~``~~~~~~``````~~`~``~~ 10
11 ~``~~~````~`~~`~~~`~~~``~``````~`~``~~~~`````~~~``````~`~`~~ 11
12 ~~~~~``~`~````~```~`~`~`~~`~~`~``~~~~~~~`~~```~~``~~`~~~~``` 12
13 `~~```~~````````~~~`~~~```~~~~~~~~`~~``~~`~```~`~~````~~~``~ 13
14 ```~``~`~`~``~```~`~``~`~``~~```~`~~~``~~``~```~`~~`~``````~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
You have found a sunken treasure chest!
You have 16 sonar device(s) left. 2 treasure chest(s) remaining.
Where do you want to drop the next sonar device? (0-59 0-14) (or type quit)
--snip--
Enter the following source code in a new file and save the file as sonar.py. Then run it by pressing F5 (or FN-F5 on OS X). If you get errors after entering this code, compare the code you typed to the book’s code with the online diff tool at https://www.nostarch.com/inventwithpython#diff.
sonar.py
1. # Sonar Treasure Hunt
2.
3. import random
4. import sys
5. import math
6.
7. def getNewBoard():
8. # Create a new 60x15 board data structure.
9. board = []
10. for x in range(60): # The main list is a list of 60 lists.
11. board.append([])
12. for y in range(15): # Each list in the main list has
15 single-character strings.
13. # Use different characters for the ocean to make it more
readable.
14. if random.randint(0, 1) == 0:
15. board[x].append('~')
16. else:
17. board[x].append('`')
18. return board
19.
20. def drawBoard(board):
21. # Draw the board data structure.
22. tensDigitsLine = ' ' # Initial space for the numbers down the left
side of the board
23. for i in range(1, 6):
24. tensDigitsLine += (' ' * 9) + str(i)
25.
26. # Print the numbers across the top of the board.
27. print(tensDigitsLine)
28. print(' ' + ('0123456789' * 6))
29. print()
30.
31. # Print each of the 15 rows.
32. for row in range(15):
33. # Single-digit numbers need to be padded with an extra space.
34. if row < 10:
35. extraSpace = ' '
36. else:
37. extraSpace = ''
38.
39. # Create the string for this row on the board.
40. boardRow = ''
41. for column in range(60):
42. boardRow += board[column][row]
43.
44. print('%s%s %s %s' % (extraSpace, row, boardRow, row))
45.
46. # Print the numbers across the bottom of the board.
47. print()
48. print(' ' + ('0123456789' * 6))
49. print(tensDigitsLine)
50.
51. def getRandomChests(numChests):
52. # Create a list of chest data structures (two-item lists of x, y int
coordinates).
53. chests = []
54. while len(chests) < numChests:
55. newChest = [random.randint(0, 59), random.randint(0, 14)]
56. if newChest not in chests: # Make sure a chest is not already
here.
57. chests.append(newChest)
58. return chests
59.
60. def isOnBoard(x, y):
61. # Return True if the coordinates are on the board; otherwise, return
False.
62. return x >= 0 and x <= 59 and y >= 0 and y <= 14
63.
64. def makeMove(board, chests, x, y):
65. # Change the board data structure with a sonar device character.
Remove treasure chests from the chests list as they are found.
66. # Return False if this is an invalid move.
67. # Otherwise, return the string of the result of this move.
68. smallestDistance = 100 # Any chest will be closer than 100.
69. for cx, cy in chests:
70. distance = math.sqrt((cx - x) * (cx - x) + (cy - y) * (cy - y))
71.
72. if distance < smallestDistance: # We want the closest treasure
chest.
73. smallestDistance = distance
74.
75. smallestDistance = round(smallestDistance)
76.
77. if smallestDistance == 0:
78. # xy is directly on a treasure chest!
79. chests.remove([x, y])
80. return 'You have found a sunken treasure chest!'
81. else:
82. if smallestDistance < 10:
83. board[x][y] = str(smallestDistance)
84. return 'Treasure detected at a distance of %s from the sonar
device.' % (smallestDistance)
85. else:
86. board[x][y] = 'X'
87. return 'Sonar did not detect anything. All treasure chests
out of range.'
88.
89. def enterPlayerMove(previousMoves):
90. # Let the player enter their move. Return a two-item list of int
xy coordinates.
91. print('Where do you want to drop the next sonar device? (0-59 0-14)
(or type quit)')
92. while True:
93. move = input()
94. if move.lower() == 'quit':
95. print('Thanks for playing!')
96. sys.exit()
97.
98. move = move.split()
99. if len(move) == 2 and move[0].isdigit() and move[1].isdigit() and
isOnBoard(int(move[0]), int(move[1])):
100. if [int(move[0]), int(move[1])] in previousMoves:
101. print('You already moved there.')
102. continue
103. return [int(move[0]), int(move[1])]
104.
105. print('Enter a number from 0 to 59, a space, then a number from
0 to 14.')
106.
107. def showInstructions():
108. print('''Instructions:
109. You are the captain of the Simon, a treasure-hunting ship. Your current
mission
110. is to use sonar devices to find three sunken treasure chests at the
bottom of
111. the ocean. But you only have cheap sonar that finds distance, not
direction.
112.
113. Enter the coordinates to drop a sonar device. The ocean map will be
marked with
114. how far away the nearest chest is, or an X if it is beyond the sonar
device's
115. range. For example, the C marks are where chests are. The sonar device
shows a
116. 3 because the closest chest is 3 spaces away.
117.
118. 1 2 3
119. 012345678901234567890123456789012
120.
121. 0 ~~~~`~```~`~``~~~``~`~~``~~~``~`~ 0
122. 1 ~`~`~``~~`~```~~~```~~`~`~~~`~~~~ 1
123. 2 `~`C``3`~~~~`C`~~~~`````~~``~~~`` 2
124. 3 ````````~~~`````~~~`~`````~`~``~` 3
125. 4 ~`~~~~`~~`~~`C`~``~~`~~~`~```~``~ 4
126.
127. 012345678901234567890123456789012
128. 1 2 3
129. (In the real game, the chests are not visible in the ocean.)
130.
131. Press enter to continue...''')
132. input()
133.
134. print('''When you drop a sonar device directly on a chest, you
retrieve it and the other
135. sonar devices update to show how far away the next nearest chest is. The
chests
136. are beyond the range of the sonar device on the left, so it shows an X.
137.
138. 1 2 3
139. 012345678901234567890123456789012
140.
141. 0 ~~~~`~```~`~``~~~``~`~~``~~~``~`~ 0
142. 1 ~`~`~``~~`~```~~~```~~`~`~~~`~~~~ 1
143. 2 `~`X``7`~~~~`C`~~~~`````~~``~~~`` 2
144. 3 ````````~~~`````~~~`~`````~`~``~` 3
145. 4 ~`~~~~`~~`~~`C`~``~~`~~~`~```~``~ 4
146.
147. 012345678901234567890123456789012
148. 1 2 3
149.
150. The treasure chests don't move around. Sonar devices can detect treasure
chests
151. up to a distance of 9 spaces. Try to collect all 3 chests before running
out of
152. sonar devices. Good luck!
153.
154. Press enter to continue...''')
155. input()
156.
157.
158.
159. print('S O N A R !')
160. print()
161. print('Would you like to view the instructions? (yes/no)')
162. if input().lower().startswith('y'):
163. showInstructions()
164.
165. while True:
166. # Game setup
167. sonarDevices = 20
168. theBoard = getNewBoard()
169. theChests = getRandomChests(3)
170. drawBoard(theBoard)
171. previousMoves = []
172.
173. while sonarDevices > 0:
174. # Show sonar device and chest statuses.
175. print('You have %s sonar device(s) left. %s treasure chest(s)
remaining.' % (sonarDevices, len(theChests)))
176.
177. x, y = enterPlayerMove(previousMoves)
178. previousMoves.append([x, y]) # We must track all moves so that
sonar devices can be updated.
179.
180. moveResult = makeMove(theBoard, theChests, x, y)
181. if moveResult == False:
182. continue
183. else:
184. if moveResult == 'You have found a sunken treasure chest!':
185. # Update all the sonar devices currently on the map.
186. for x, y in previousMoves:
187. makeMove(theBoard, theChests, x, y)
188. drawBoard(theBoard)
189. print(moveResult)
190.
191. if len(theChests) == 0:
192. print('You have found all the sunken treasure chests!
Congratulations and good game!')
193. break
194.
195. sonarDevices -= 1
196.
197. if sonarDevices == 0:
198. print('We\'ve run out of sonar devices! Now we have to turn the
ship around and head')
199. print('for home with treasure chests still out there! Game
over.')
200. print(' The remaining chests were here:')
201. for x, y in theChests:
202. print(' %s, %s' % (x, y))
203.
204. print('Do you want to play again? (yes or no)')
205. if not input().lower().startswith('y'):
206. sys.exit()
Before trying to understand the source code, play the game a few times to learn what is going on. The Sonar Treasure Hunt game uses lists of lists and other complicated variables, called data structures. Data structures store arrangements of values to represent something. For example, in Chapter 10, a Tic-Tac-Toe board data structure was a list of strings. The string represented an X, an O, or an empty space, and the index of the string in the list represented the space on the board. The Sonar Treasure Hunt game will have similar data structures for the locations of treasure chests and sonar devices.
At the start of the program, we import the random, sys, and math modules:
1. # Sonar Treasure Hunt
2.
3. import random
4. import sys
5. import math
The sys module contains the exit() function, which terminates the program immediately. None of the lines of code after the sys.exit() call will run; the program just stops as though it has reached the end. This function is used later in the program.
The math module contains the sqrt() function, which is used to find the square root of a number. The math behind square roots is explained the “Finding the Closest Treasure Chest” on page 186.
The start of each new game requires a new board data structure, which is created by getNewBoard(). The Sonar Treasure Hunt game board is an ASCII art ocean with x- and y-coordinates around it.
When we use the board data structure, we want to be able to access its coordinate system in the same way we access Cartesian coordinates. To do that, we’ll use a list of lists to call each coordinate on the board like this: board[x][y]. The x-coordinate comes before the y-coordinate—to get the string at coordinate (26, 12), you access board[26][12], not board[12][26].
7. def getNewBoard():
8. # Create a new 60x15 board data structure.
9. board = []
10. for x in range(60): # The main list is a list of 60 lists.
11. board.append([])
12. for y in range(15): # Each list in the main list has
15 single-character strings.
13. # Use different characters for the ocean to make it more
readable.
14. if random.randint(0, 1) == 0:
15. board[x].append('~')
16. else:
17. board[x].append('`')
The board data structure is a list of lists of strings. The first list represents the x-coordinate. Since the game’s board is 60 characters across, this first list needs to contain 60 lists. At line 10, we create a for loop that will append 60 blank lists to it.
But board is more than just a list of 60 blank lists. Each of the 60 lists represents an x-coordinate of the game board. There are 15 rows in the board, so each of these 60 lists must contain 15 strings. Line 12 is another for loop that adds 15 single-character strings that represent the ocean.
The ocean will be a bunch of randomly chosen '~' and '`' strings. The tilde (~) and backtick (`) characters—located next to the 1 key on your keyboard—will be used for the ocean waves. To determine which character to use, lines 14 to 17 apply this logic: if the return value of random.randint() is 0, add the '~' string; otherwise, add the '`' string. This will give the ocean a random, choppy look.
For a smaller example, if board were set to [['~', '~', '`'], [['~', '~', '`'], [['~', '~', '`'], ['~', '`', '`'], ['`', '~', '`']] then the board it drew would look like this:
~~~~`
~~~`~
`````
Finally, the function returns the value in the board variable on line 18:
18. return board
Next we’ll define the drawBoard() method that we call whenever we actually draw a new board:
20. def drawBoard(board):
The full game board with coordinates along the edges looks like this:
1 2 3 4 5
012345678901234567890123456789012345678901234567890123456789
0 ~~~`~``~~~``~~~~``~`~`~`~`~~`~~~`~~`~``````~~`~``~`~~```~`~` 0
1 `~`~````~~``~`~```~```~```~`~~~``~~`~~~``````~`~``~~``~~`~~` 1
2 ```~~~~`~`~~```~~~``~````~~`~`~~`~`~`~```~~`~``~~`~`~~~~~~`~ 2
3 ~~~~`~~~``~```~``~~`~`~~`~`~~``~````~`~````~```~`~`~`~`````~ 3
4 ~```~~~~~`~~````~~~~```~~~`~`~`~````~`~~`~`~~``~~`~``~`~``~~ 4
5 `~```~`~`~~`~~~```~~``~``````~~``~`~`~~~~`~~``~~~~~~`~```~~` 5
6 ``~~`~~`~``~`````~````~~``~`~~~~`~~```~~~``~`~`~~``~~~```~~~ 6
7 ``~``~~~~~~```~`~```~~~``~`~``~`~~~~~~```````~~~`~~`~~`~~`~~ 7
8 ~~`~`~~```~``~~``~~~``~~`~`~~`~`~```~```~~~```~~~~~~`~`~~~~` 8
9 ```~``~`~~~`~~```~``~``~~~```~````~```~`~~`~~~~~`~``~~~~~``` 9
10 `~~~~```~`~````~`~`~~``~`~~~~`~``~``~```~~```````~`~``~````` 10
11 ~~`~`~~`~``~`~~~````````````````~~`````~`~~``~`~~~~`~~~`~~`~ 11
12 ~~`~~~~```~~~`````~~``~`~`~~``````~`~~``~```````~~``~~~`~~`~ 12
13 `~``````~~``~`~~~```~~~~```~~`~`~~~`~```````~~`~```~``~`~~~~ 13
14 ~~~``~```~`````~~`~`~``~~`~``~`~~`~`~``~`~``~~``~`~``~```~~~ 14
012345678901234567890123456789012345678901234567890123456789
1 2 3 4 5
The drawing in the drawBoard() function has four steps:
Create a string variable of the line with 1, 2, 3, 4, and 5 spaced out with wide gaps. These numbers mark the coordinates for 10, 20, 30, 40, and 50 on the x-axis.
Use that string to display the x-axis coordinates along the top of the screen.
Print each row of the ocean along with the y-axis coordinates on both sides of the screen.
Print the x-axis again at the bottom. Having coordinates on all sides makes it easier to see where to place a sonar device.
The first part of drawBoard() prints the x-axis at the top of the board. Because we want each part of the board to be even, each coordinate label can take up only one character space. When the coordinate numbering reaches 10, there are two digits for each number, so we put the digits in the tens place on a separate line, as shown in Figure 13-3. The x-axis is organized so that the first line shows the tens-place digits and the second line shows the onesplace digits.
Figure 13-3: The spacing used for printing the top of the game board
Lines 22 to 24 create the string for the first line of the board, which is the tens-place part of the x-axis:
21. # Draw the board data structure.
22. tensDigitsLine = ' ' # Initial space for the numbers down the left
side of the board
23. for i in range(1, 6):
24. tensDigitsLine += (' ' * 9) + str(i)
The numbers marking the tens position on the first line all have 9 spaces between them, and there are 13 spaces in front of the 1. Lines 22 to 24 create a string with this line and store it in a variable named tensDigitsLine:
26. # Print the numbers across the top of the board.
27. print(tensDigitsLine)
28. print(' ' + ('0123456789' * 6))
29. print()
To print the numbers across the top of the game board, first print the contents of the tensDigitsLine variable. Then, on the next line, print three spaces (so that this row lines up correctly), and then print the string '0123456789' six times: ('0123456789' * 6).
Lines 32 to 44 print each row of the ocean waves, including the numbers down the sides to label the y-axis:
31. # Print each of the 15 rows.
32. for row in range(15):
33. # Single-digit numbers need to be padded with an extra space.
34. if row < 10:
35. extraSpace = ' '
36. else:
37. extraSpace = ''
The for loop prints rows 0 to 14, along with the row numbers on either side of the board.
But we have the same problem that we had with the x-axis. Numbers with only one digit (such as 0, 1, 2, and so on) take up only one space when printed, but numbers with two digits (such as 10, 11, and 12) take up two spaces. The rows won’t line up if the coordinates have different sizes. The board would look like this:
8 ~~`~`~~```~``~~``~~~``~~`~`~~`~`~```~```~~~```~~~~~~`~`~~~~` 8
9 ```~``~`~~~`~~```~``~``~~~```~````~```~`~~`~~~~~`~``~~~~~``` 9
10 `~~~~```~`~````~`~`~~``~`~~~~`~``~``~```~~```````~`~``~````` 10
11 ~~`~`~~`~``~`~~~````````````````~~`````~`~~``~`~~~~`~~~`~~`~ 11
The solution is easy: add a space in front of all the single-digit numbers. Lines 34 to 37 set the variable extraSpace to either a space or an empty string. The extraSpace variable is always printed, but it has a space character only for single-digit row numbers. Otherwise, it is an empty string. This way, all of the rows will line up when you print them.
The board parameter is a data structure for the entire ocean’s waves. Lines 39 to 44 read the board variable and print a single row:
39. # Create the string for this row on the board.
40. boardRow = ''
41. for column in range(60):
42. boardRow += board[column][row]
43.
44. print('%s%s %s %s' % (extraSpace, row, boardRow, row))
At line 40, boardRow starts as a blank string. The for loop on line 32 sets the row variable for the current row of ocean waves to print. Inside this loop on line 41 is another for loop that iterates over each column of the current row. We make boardRow by concatenating board[column][row] in this loop, which means concatenating board[0][row], board[1][row], board[2][row], and so on up to board[59][row]. This is because the row contains 60 characters, from index 0 to index 59.
The for loop on line 41 iterates over integers 0 to 59. On each iteration, the next character in the board data structure is copied to the end of boardRow. By the time the loop is done, boardRow has the row’s complete ASCII art waves. The string in boardRow is then printed along with the row numbers on line 44.
Lines 46 to 49 are similar to lines 26 to 29:
46. # Print the numbers across the bottom of the board.
47. print()
48. print(' ' + ('0123456789' * 6))
49. print(tensDigitsLine)
These lines print the x-coordinates at the bottom of the board.
The game randomly decides where the hidden treasure chests are. The treasure chests are represented as a list of lists of two integers. These two integers are the x- and y-coordinates of a single chest. For example, if the chest data structure were [[2, 2], [2, 4], [10, 0]], then this would mean there were three treasure chests, one at (2, 2), another at (2, 4), and a third at (10, 0).
The getRandomChests() function creates a certain number of chest data structures at randomly assigned coordinates:
51. def getRandomChests(numChests):
52. # Create a list of chest data structures (two-item lists of x, y int
coordinates).
53. chests = []
54. while len(chests) < numChests:
55. newChest = [random.randint(0, 59), random.randint(0, 14)]
56. if newChest not in chests: # Make sure a chest is not already
here.
57. chests.append(newChest)
58. return chests
The numChests parameter tells the function how many treasure chests to generate. Line 54’s while loop will iterate until all of the chests have been assigned coordinates. Two random integers are selected for the coordinates on line 55. The x-coordinate can be anywhere from 0 to 59, and the y-coordinate can be anywhere from 0 to 14. The [random.randint(0, 59), random.randint(0, 14)] expression will evaluate to a list value like [2, 2] or [2, 4] or [10, 0]. If these coordinates do not already exist in the chests list, they are appended to chests on line 57.
When the player enters the x- and y-coordinates for where they want to drop a sonar device, we need to make sure that the numbers are valid. As mentioned before, there are two conditions for a move to be valid: the x-coordinate must be between 0 and 59, and the y-coordinate must be between 0 and 14.
The isOnBoard() function uses a simple expression with and operators to combine these conditions into one expression and to ensure that each part of the expression is True:
60. def isOnBoard(x, y):
61. # Return True if the coordinates are on the board; otherwise, return
False.
62. return x >= 0 and x <= 59 and y >= 0 and y <= 14
Because we are using the and Boolean operator, if even one of the coordinates is invalid, then the entire expression evaluates to False.
In the Sonar Treasure Hunt game, the game board is updated to display a number that represents each sonar device’s distance to the closest treasure chest. So when the player makes a move by giving the program an x- and y-coordinate, the board changes based on the positions of the treasure chests.
64. def makeMove(board, chests, x, y):
65. # Change the board data structure with a sonar device character.
Remove treasure chests from the chests list as they are found.
66. # Return False if this is an invalid move.
67. # Otherwise, return the string of the result of this move.
The makeMove() function takes four parameters: the game board’s data structure, the treasure chest’s data structure, the x-coordinate, and the y-coordinate. The makeMove() function will return a string value describing what happened in response to the move:
• If the coordinates land directly on a treasure chest, makeMove() returns 'You have found a sunken treasure chest!'.
• If the coordinates are within a distance of 9 or less of a chest, makeMove() returns 'Treasure detected at a distance of %s from the sonar device.' (where %s is replaced with the integer distance).
• Otherwise, makeMove() will return 'Sonar did not detect anything. All treasure chests out of range.'.
Given the coordinates of where the player wants to drop the sonar device and a list of x- and y-coordinates for the treasure chests, you’ll need an algorithm to find out which treasure chest is closest.
Lines 68 to 75 are an algorithm to determine which treasure chest is closest to the sonar device.
68. smallestDistance = 100 # Any chest will be closer than 100.
69. for cx, cy in chests:
70. distance = math.sqrt((cx - x) * (cx - x) + (cy - y) * (cy - y))
71.
72. if distance < smallestDistance: # We want the closest treasure
chest.
73. smallestDistance = distance
The x and y parameters are integers (say, 3 and 5), and together they represent the location on the game board where the player guessed. The chests variable will have a value such as [[5, 0], [0, 2], [4, 2]], which represents the locations of three treasure chests. Figure 13-4 illustrates this value.
To find the distance between the sonar device and a treasure chest, we’ll need to do some math to find the distance between two x- and y-coordinates. Let’s say we place a sonar device at (3, 5) and want to find the distance to the treasure chest at (4, 2).
Figure 13-4: The treasure chests represented by [[5, 0], [0, 2], [4, 2]]
To find the distance between two sets of x- and y-coordinates, we’ll use the Pythagorean theorem. This theorem applies to right triangles—triangles where one corner is 90 degrees, the same kind of corner you find in a rectangle. The Pythagorean theorem says that the diagonal side of the triangle can be calculated from the lengths of the horizontal and vertical sides. Figure 13-5 shows a right triangle drawn between the sonar device at (3, 5) and the treasure chest at (4, 2).
Figure 13-5: The board with a right triangle drawn over the sonar device and a treasure chest
The Pythagorean theorem is a2 + b2 = c2, in which a is the length of the horizontal side, b is the length of the vertical side, and c is the length of the diagonal side, or hypotenuse. These lengths are squared, which means that number is multiplied by itself. “Unsquaring” a number is called finding the number’s square root, as we’ll have to do to get c from c2.
Let’s use the Pythagorean theorem to find the distance between the sonar device at (3, 5) and chest at (4, 2):
To find a, subtract the second x-coordinate, 4, from the first x-coordinate, 3: 3 – 4 = –1.
To find a2, multiply a by a: –1 × –1 = 1. (A negative number times a negative number is always a positive number.)
To find b, subtract the second y-coordinate, 2, from the first y-coordinate, 5: 5 – 2 = 3.
To find b2, multiply b by b: 3 × 3 = 9.
To find c2, add a2 and b2: 1 + 9 = 10.
To get c from c2, you need to find the square root of c2.
The math module that we imported on line 5 has a square root function named sqrt(). Enter the following into the interactive shell:
>>> import math
>>> math.sqrt(10)
3.1622776601683795
>>> 3.1622776601683795 * 3.1622776601683795
10.000000000000002
Notice that multiplying a square root by itself produces the square number. (The extra 2 at the end of the 10 is from an unavoidable slight imprecision in the sqrt() function.)
By passing c2 to sqrt(), we can tell that the sonar device is 3.16 units away from the treasure chest. The game will round this down to 3.
Let’s look at lines 68 to 70 again:
68. smallestDistance = 100 # Any chest will be closer than 100.
69. for cx, cy in chests:
70. distance = math.sqrt((cx - x) * (cx - x) + (cy - y) * (cy - y))
The code inside line 69’s for loop calculates the distance of each chest. Line 68 gives smallestDistance the impossibly long distance of 100 at the beginning of the loop so that at least one of the treasure chests you find will be put into smallestDistance in line 73. Since cx – x represents the horizontal distance a between the chest and sonar device, (cx - x) * (cx - x) is the a2 of our Pythagorean theorem calculation. It is added to (cy - y) * (cy - y), the b2. This sum is c2 and is passed to sqrt() to get the distance between the chest and sonar device.
We want to find the distance between the sonar device and the closest chest, so if this distance is less than the smallest distance, it is saved as the new smallest distance on line 73:
72. if distance < smallestDistance: # We want the closest treasure
chest.
73. smallestDistance = distance
By the time the for loop has finished, you know that smallestDistance holds the shortest distance between the sonar device and all of the treasure chests in the game.
The remove() list method removes the first occurrence of a value matching the passed-in argument. For example, enter the following into the interactive shell:
>>> x = [42, 5, 10, 42, 15, 42]
>>> x.remove(10)
>>> x
[42, 5, 42, 15, 42]
The 10 value has been removed from the x list.
Now enter the following into the interactive shell:
>>> x = [42, 5, 10, 42, 15, 42]
>>> x.remove(42)
>>> x
[5, 10, 42, 15, 42]
Notice that only the first 42 value was removed and the second and third ones are still there. The remove() method removes the first, and only the first, occurrence of the value you pass it.
If you try to remove a value that isn’t in the list, you’ll get an error:
>>> x = [5, 42]
>>> x.remove(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list
Like the append() method, the remove() method is called on a list and does not return a list. You want to use code like x.remove(42), not x = x.remove(42).
Let’s go back to finding the distances between sonar devices and treasure chests in the game. The only time that smallestDistance is equal to 0 is when the sonar device’s x- and y-coordinates are the same as a treasure chest’s x- and y-coordinates. This means the player has correctly guessed the location of a treasure chest.
77. if smallestDistance == 0:
78. # xy is directly on a treasure chest!
79. chests.remove([x, y])
80. return 'You have found a sunken treasure chest!'
When this happens, the program removes this chest’s two-integer list from the chests data structure with the remove() list method. Then the function returns 'You have found a sunken treasure chest!'.
But if smallestDistance is not 0, the player didn’t guess an exact location of a treasure chest, and the else block starting on line 81 executes:
81. else:
82. if smallestDistance < 10:
83. board[x][y] = str(smallestDistance)
84. return 'Treasure detected at a distance of %s from the sonar
device.' % (smallestDistance)
85. else:
86. board[x][y] = 'X'
87. return 'Sonar did not detect anything. All treasure chests
out of range.'
If the sonar device’s distance to a treasure chest is less than 10, line 83 marks the board with the string version of smallestDistance. If not, the board is marked with an 'X'. This way, the player knows how close each sonar device is to a treasure chest. If the player sees a 0, they know they’re way off.
The enterPlayerMove() function collects the x- and y-coordinates of the player’s next move:
89. def enterPlayerMove(previousMoves):
90. # Let the player enter their move. Return a two-item list of int
xy coordinates.
91. print('Where do you want to drop the next sonar device? (0-59 0-14)
(or type quit)')
92. while True:
93. move = input()
94. if move.lower() == 'quit':
95. print('Thanks for playing!')
96. sys.exit()
The previousMoves parameter is a list of two-integer lists of the previous places the player put a sonar device. This information will be used so that the player cannot place a sonar device somewhere they have already put one.
The while loop will keep asking the player for their next move until they enter coordinates for a place that doesn’t already have a sonar device. The player can also enter 'quit' to quit the game. In that case, line 96 calls the sys.exit() function to terminate the program immediately.
Assuming the player has not entered 'quit', the code checks that the input is two integers separated by a space. Line 98 calls the split() method on move as the new value of move:
98. move = move.split()
99. if len(move) == 2 and move[0].isdigit() and move[1].isdigit() and
isOnBoard(int(move[0]), int(move[1])):
100. if [int(move[0]), int(move[1])] in previousMoves:
101. print('You already moved there.')
102. continue
103. return [int(move[0]), int(move[1])]
104.
105. print('Enter a number from 0 to 59, a space, then a number from
0 to 14.')
If the player typed in a value like '1 2 3', then the list returned by split() would be ['1', '2', '3']. In that case, the expression len(move) == 2 would be False (the list in move should be only two numbers because it represents a coordinate), and the entire expression would evaluate immediately to False. Python doesn’t check the rest of the expression because of short-circuiting (which was described in “Short-Circuit Evaluation” on page 139).
If the list’s length is 2, then the two values will be at indexes move[0] and move[1]. To check whether those values are numeric digits (like '2' or '17'), you could use a function like isOnlyDigits() from “Checking Whether a String Has Only Numbers” on page 158. But Python already has a method that does this.
The string method isdigit() returns True if the string consists solely of numbers. Otherwise, it returns False. Enter the following into the interactive shell:
>>> '42'.isdigit()
True
>>> 'forty'.isdigit()
False
>>> ''.isdigit()
False
>>> 'hello'.isdigit()
False
>>> x = '10'
>>> x.isdigit()
True
Both move[0].isdigit() and move[1].isdigit() must be True for the whole condition to be True. The final part of line 99’s condition calls the isOnBoard() function to check whether the x- and y-coordinates exist on the board.
If the entire condition is True, line 100 checks whether the move exists in the previousMoves list. If it does, then line 102’s continue statement causes the execution to go back to the start of the while loop at line 92 and then ask for the player’s move again. If it doesn’t, line 103 returns a two-integer list of the x- and y-coordinates.
The showInstructions() function is a couple of print() calls that print multiline strings:
107. def showInstructions():
108. print('''Instructions:
109. You are the captain of the Simon, a treasure-hunting ship. Your current
mission
--snip--
154. Press enter to continue...''')
155. input()
The input() function gives the player a chance to press ENTER before printing the next string. This is because the IDLE window can show only so much text at a time, and we don’t want the player to have to scroll up to read the beginning of the text. After the player presses ENTER, the function returns to the line that called the function.
Now that we’ve entered all the functions that our game will call, let’s enter the main part of the game. The first thing the player sees after running the program is the game title printed by line 159. This is the main part of the program, which begins by offering the player instructions and then setting up the variables the game will use.
159. print('S O N A R !')
160. print()
161. print('Would you like to view the instructions? (yes/no)')
162. if input().lower().startswith('y'):
163. showInstructions()
164.
165. while True:
166. # Game setup
167. sonarDevices = 20
168. theBoard = getNewBoard()
169. theChests = getRandomChests(3)
170. drawBoard(theBoard)
171. previousMoves = []
The expression input().lower().startswith('y') lets the player request the instructions, and it evaluates to True if the player enters a string that begins with 'y' or 'Y'. For example:
If this condition is True, showInstructions() is called on line 163. Otherwise, the game begins.
Several variables are set up on lines 167 to 171; these are described in Table 13-1.
Table 13-1: Variables Used in the Main Game Loop
Variable |
Description |
sonarDevices |
The number of sonar devices (and turns) the player has left. |
theBoard |
The board data structure used for this game. |
theChests |
The list of chest data structures. getRandomChests() returns a list of three treasure chests at random places on the board. |
previousMoves |
A list of all the x and y moves the player has made in the game. |
We’re going to use these variables soon, so make sure to review their descriptions before moving on!
Line 173’s while loop executes as long as the player has sonar devices remaining and prints a message telling them how many sonar devices and treasure chests are left:
173. while sonarDevices > 0:
174. # Show sonar device and chest statuses.
175. print('You have %s sonar device(s) left. %s treasure chest(s)
remaining.' % (sonarDevices, len(theChests)))
After printing how many devices are left, the while loop continues to execute.
Line 177 is still part of the while loop and uses multiple assignment to assign the x and y variables to the two-item list representing the player’s move coordinates returned by enterPlayerMove(). We’ll pass in previousMoves so that enterPlayerMove()’s code can ensure the player doesn’t repeat a previous move.
177. x, y = enterPlayerMove(previousMoves)
178. previousMoves.append([x, y]) # We must track all moves so that
sonar devices can be updated.
179.
180. moveResult = makeMove(theBoard, theChests, x, y)
181. if moveResult == False:
182. continue
The x and y variables are then appended to the end of the previousMoves list. The previousMoves variable is a list of x- and y-coordinates of each move the player makes in this game. This list is used later in the program on lines 177 and 186.
The x, y, theBoard, and theChests variables are all passed to the makeMove() function at line 180. This function makes the necessary modifications to place a sonar device on the board.
If makeMove() returns False, then there was a problem with the x and y values passed to it. The continue statement sends the execution back to the start of the while loop on line 173 to ask the player for x- and y-coordinates again.
If makeMove() doesn’t return False, it returns a string of the results of that move. If this string is 'You have found a sunken treasure chest!', then all the sonar devices on the board should update to detect the next closest treasure chest on the board:
183. else:
184. if moveResult == 'You have found a sunken treasure chest!':
185. # Update all the sonar devices currently on the map.
186. for x, y in previousMoves:
187. makeMove(theBoard, theChests, x, y)
188. drawBoard(theBoard)
189. print(moveResult)
The x- and y-coordinates of all the sonar devices are in previousMoves. By iterating over previousMoves on line 186, you can pass all of these x- and y-coordinates to the makeMove() function again to redraw the values on the board. Because the program doesn’t print any new text here, the player doesn’t realize the program is redoing all of the previous moves. It just appears that the board updates itself.
Remember that the makeMove() function modifies the theChests list you sent it. Because theChests is a list, any changes made to it inside the function will persist after execution returns from the function. The makeMove() function removes items from theChests when treasure chests are found, so eventually (if the player keeps guessing correctly) all of the treasure chests will have been removed. (Remember, by “treasure chest” we mean the two-item lists of the x- and y-coordinates inside the theChests list.)
191. if len(theChests) == 0:
192. print('You have found all the sunken treasure chests!
Congratulations and good game!')
193. break
When all the treasure chests have been found on the board and removed from theChests, the theChests list will have a length of 0. When that happens, the code displays a congratulatory message to the player and then executes a break statement to break out of this while loop. Execution will then move to line 197, the first line after the while block.
Line 195 is the last line of the while loop that started on line 173.
195. sonarDevices -= 1
The program decrements the sonarDevices variable because the player has used one sonar device. If the player keeps missing the treasure chests, eventually sonarDevices will be reduced to 0. After this line, execution jumps back up to line 173 so it can reevaluate the while statement’s condition (which is sonarDevices > 0).
If sonarDevices is 0, then the condition will be False and execution will continue outside the while block on line 197. But until then, the condition will remain True and the player can keep making guesses:
197. if sonarDevices == 0:
198. print('We\'ve run out of sonar devices! Now we have to turn the
ship around and head')
199. print('for home with treasure chests still out there! Game
over.')
200. print(' The remaining chests were here:')
201. for x, y in theChests:
202. print(' %s, %s' % (x, y))
Line 197 is the first line outside the while loop. When the execution reaches this point, the game is over. If sonarDevices is 0, you know the player ran out of sonar devices before finding all the chests and lost.
Lines 198 to 200 will tell the player they’ve lost. The for loop on line 201 will go through the treasure chests remaining in theChests and display their location so the player can see where the treasure chests were lurking.
Win or lose, the program lets the player decide whether they want to keep playing. If the player does not enter 'yes' or 'Y' or enters some other string that doesn’t begin with the letter y, then not input().lower().startswith('y') evaluates to True and the sys.exit() function is executed. This causes the program to terminate.
204. print('Do you want to play again? (yes or no)')
205. if not input().lower().startswith('y'):
206. sys.exit()
Otherwise, execution jumps back to the beginning of the while loop on line 165 and a new game begins.
Remember how our Tic-Tac-Toe game numbered the spaces on the Tic-Tac-Toe board 1 through 9? This sort of coordinate system might have been okay for a board with fewer than 10 spaces. But the Sonar Treasure Hunt board has 900 spaces! The Cartesian coordinate system we learned about in Chapter 12 really makes all these spaces manageable, especially when our game needs to find the distance between two points on the board.
Locations in games that use a Cartesian coordinate system can be stored in a list of lists in which the first index is the x-coordinate and the second index is the y-coordinate. This makes it easy to access a coordinate using board[x][y].
These data structures (such as the ones used for the ocean and treasure chest locations) make it possible to represent complex concepts as data, and your game programs become mostly about modifying these data structures.
In the next chapter, we’ll represent letters as numbers. By representing text as numbers, we can perform math operations on them to encrypt or decrypt secret messages.