Sonar Treasure Hunt
Designing the Program
Sonar is kind of complicated, so it might be better to type in the game's code and play it a few times first to understand what is going on. After you've played the game a few times, you can kind of get an idea of the sequence of events in this game.
The Sonar game uses lists of lists and other complicated variables. These complicated variables are known as data structures. Data structures will let us store nontrivial arrangements of values in a single variable. We will use data structures for the Sonar board and the locations of the treasure chests. One example of a data structure was the board variable in the Tic Tac Toe chapter.
It is also helpful to write out the things we need our program to do, and come up with some function names that will handle these actions. Remember to name functions after what they specifically do. Otherwise we might end up forgetting a function, or typing in two different functions that do the same thing.
These might not be all of the functions we need, but a list like this is a good idea to help you get started with programming your own games. For example, when we are writing the drawBoard() function in the Sonar game, we figure out that we also need a getRow() function. Writing out a function once and then calling it twice is preferable to writing out the code twice. The whole point of functions is to reduce duplicate code down to one place, so if we ever need to make changes to that code we only need to change one place in our program.
How the Code Works: Lines 1 to 38
1. # Sonar 2. 3. import random 4. import sys
Here we import two modules, random and sys. The sys module contains the exit() function, which causes the program to immediately terminate. We will call this function later in our program.
Drawing the Game Board
6. def drawBoard(board):
The back tick (`) and tilde (~) characters are located next to the 1 key on your keyboard. They resemble the waves of the ocean. Somewhere in this ocean are three treasure chests, but you don't know where. You can figure it out by planting sonar devices, and tell the game program where by typing in the X and Y coordinates (which are printed on the four sides of the screen.)
The drawBoard() function is the first function we will define for our program. The sonar game's board is an ASCII-art ocean with coordinates going along the X- and Y-axis, and 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
We will split up the drawing in the drawBoard() function into four steps. First, we create a string variable of the line with 1, 2, 3, 4, and 5 spaced out with wide gaps. Second, we use that string to display the X-axis coordinates along the top of the screen. Third, we print each row of the ocean along with the Y-axis coordinates on both sides of the screen. And fourth, we print out the X-axis again at the bottom. Having the coordinates on all sides makes it easier for the player to move their finger along the spaces to see where exactly they want to plan a sonar device.
Drawing the X-coordinates Along the Top
7. # Draw the board data structure. 8. 9. hline = ' ' # initial space for the numbers down the left side of the board 10. for i in range(1, 6) : 11. hline += (' ' * 9) + str(i)
Let's look again at the top part of the board, this time with plus signs instead of blank spaces so we can count the spaces easier:
The numbers on the first line which mark the tens position all have nine spaces in between them, and there are thirteen spaces in front of the 1. We are going to create a string with this line and store it in a variable named hline.
13. # print the numbers across the top 14. print(hline) 15. print ( ' ' + ('0123456789' * 6)) 16. print ()
To print the numbers across the top of the sonar board, we first print the contents of the hline variable. Then on the next line, we print three spaces (so that this row lines up correctly), and then print the string '012345678901234567890123456789012345678901234567890123456789' But this is tedious to type into the source, so instead we type ('0123456789' * 6) which evaluates to the same string.
Drawing the Rows of the Ocean
18. # print each of the 15 rows 19. for i in range(15) : 20. # single-digit numbers need to be padded with an extra space 21. if i < 10 : 22. extraSpace = ' ' 23. else: 24. extraSpace = ' ' 25. print('%s%s %s %s' % (extraSpace, i, getRow (board, i), i))
Now we print the each row of the board, including the numbers down the side to label the Y-axis. We use the for loop to print rows 0 through 14 on the board, along with the row numbers on either side of the board.
We have a small problem. Numbers with only one digit (like 0, 1, 2, and so on) only take up one space when we print them out, but numbers with two digits (like 10, 11, and 12) take up two spaces. This means the rows might not line up and would look like this:
10 `~~~~```~`~````~`~`~~``~`~~~~`~``~``~```~~```````~`~``~````` 10
The solution is easy. We just add a space in front of all the single-digit numbers. The if-else statement that starts on line 21 does this. We will print the variable extraSpace when we print the row, and if i is less than 10 (which means it will have only one digit), we assign a single space string to extraSpace. Otherwise, we set extraSpace to be a blank string. This way, all of our rows will line up when we print them.
The getRow() function will return a string representing the row number we pass it. Its two parameters are the board data structure stored in the board variable and a row number. We will look at this function next.
Drawing the X-coordinates Along the Bottom
27. # print the numbers across the bottom 28. print () 29. print ( ' ' + ('0123456789' * 6)) 30. print(hline)
This code is similar to lines 14 to 17. This will print the X-axis coordinates along the bottom of the screen.
Getting the State of a Row in the Ocean
33. def getRow(board, row): 34. # Return a string from the board data structure at a certain row. 35. boardRow = '' 36. for i in range(60) : 37. boardRow += board[i][row] 38. return boardRow
This function constructs a string called boardRow from the characters stored in board. First we set boardRow to the blank string. The row number (which is the Y coordinate) is passed as a parameter. The string we want is made by concatenating board[0][row], board[1][row], board[2][row], and so on up to board[59][row]. (This is because the row is made up of 60 characters, from index 0 to index 59.)
The for loop iterates from integers 0 to 59. On each iteration the next character in the board data structure is copied on to the end of boardRow. By the time the loop is done, extraSpace is fully formed, so we return it.
How the Code Works: Lines 40 to 62
Now that we have a function to print a given game board data structure to the string, let's turn to the other functions that we will need. At the start of the game, we will need to create a new game board data structure and also place treasure chests randomly around the board. We should also create a function that can tell if the coordinates entered by the player are a valid move or not. Creating a New Game Board
40. def getNewBoard(): 41. # Create a new 60x15 board data structure. 42. board = [] 43. for x in range(60): # the main list is a list of 60 lists 44. board.append([])
At the start of each new game, we will need a fresh board data structure. The board data structure is a list of lists of strings. The first list represents the X coordinate. Since our game's board is 60 characters across, this first list needs to contain 60 lists. So we create a for loop that will append 60 blank lists to it.
45. for y in range(15): # each list in the main list has 15 single-character strings 46. # use different characters for the ocean to make it more readable. 47. if random.randint(0, 1) == 0: 48. board[x].append('~') 49. else: 50 . board [x] . append (' `' )
But board is more than just a list of 60 blank lists. Each of the 60 lists represents the Y coordinate of our game board. There are 15 rows in the board, so each of these 60 lists must have 15 characters in them. We have another for loop to add 15 single-character strings that represent the ocean. The "ocean" will just be a bunch of '~' and '`' strings, so we will randomly choose between those two. We can do this by generating a random number between 0 and 1 with a call to random.randint(). If the return value of random.randint() is 0, we add the '~' string. Otherwise we will add the '`' string.
This is like deciding which character to use by tossing a coin. And since the return value from random.randint() will be 0 about half the time, half of the ocean characters will be '~' and the other half will be '`'. This will give our ocean a random, choppy look to it.
Remember that the board variable is a list of 60 lists that have 15 strings. That means to get the string at coordinate 26, 12, we would access board[26][12], and not board [12][26]. The X coordinate is first, then the Y coordinate.
Here is the picture to demonstrate the indexes of a list of lists named x. The red arrows point to indexes of the inner lists themselves. The image is also flipped on its side to make it easier to read:
51. return board
Finally, we return the board variable. Remember that in this case, we are returning a reference to the list that we made. Any changes we made to the list (or the lists inside the list) in our function will still be there outside of the function.
Creating the Random Treasure Chests
53. def getRandomChests(numChests): 54. # Create a list of chest data structures (two-item lists of x, y int coordinates) 55. chests = [] 56. for i in range(numChests) : 57. chests.append([random.randint(0 , 59), random.randint(0, 14)]) 58 . return chests
Another task we need to do at the start of the game is decide where the hidden treasure chests are. We will represent the treasure chests in our game as a list of lists of two integers. These two integers will be the X and Y coordinates. For example, if the chest data structure was [[2, 2], [2, 4], [10, 0]], then this would mean there are three treasure chests, one at 2, 2, another at 2, 4, and a third one at 10, 0.
We will pass the numChests parameter to tell the function how many treasure chests we want it to generate. We set up a for loop to iterate this number of times, and on each iteration we append a list of two random integers. The X coordinate can be anywhere from 0 to 59, and the Y coordinate can be from anywhere between 0 and 14. The expression [random.randint(0, 59), random.randint(0, 14)] that is passed to the append method will evaluate to something like [2, 2] or [2, 4] or [10, 0]. This data structure is then returned.
Determining if a Move is Valid
60. def isValidMove(x, y): 61. # Return True if the coordinates are on the board, otherwise False. 62. return x >= 0 and x <= 59 and y >= 0 and y <= 14
The player will type in X and Y coordinates of where they want to drop a sonar device. But they may not type in coordinates that do not exist on the game board. The X coordinates must be between 0 and 59, and the Y coordinate must be between 0 and 14. This function uses a simple expression that uses and operators to ensure that each condition is True. If just one is False, then the entire expression evaluates to False. This Boolean value is returned by the function.