Original address: http://inventwithpython.com/pygame/chapter3.html
Memory puzzle
How to play memory puzzles
In the Memory Puzzle game, several icons are covered with white boxes. Each icon has two. Players can click on both boxes to see the icons behind them. If the icons match, the boxes remain uncovered. When all the boxes on the chessboard are opened, the player wins. In order to give players a hint, these boxes will be opened soon at the beginning of the game.
Nested loop
A concept that you will see in the memory jigsaw puzzle (and most of the Games in this book) is the use of another cycle within the cycle. These are called nested for loops. Nested for loops are convenient for traversing every possible combination of two lists.
In the Memory Puzzle code, we need to iterate over every possible X and Y coordinate on the board several times. We will use nested for loops to ensure that we get each combination. Note that the inner for loop (another for loop within the for loop) will complete all its iterations before entering the next iteration of the outer for loop. If we reverse the order of the for loop, the same values will be printed, but they will be printed in a different order.
Memory puzzle source code
The source code can be downloaded from http://invpy.com/memorypuzzle.py Download.
To continue, first enter the whole program in the file editor of IDLE and save it as memory puzzle Py, and then run it. If you receive any error messages, check the line numbers mentioned in the error messages and check your code for any spelling errors. You can also copy and paste your code into http://invpy.com/diff/memorypuzzle To see if there are any differences between your code and the code in this book.
With just one input, you may learn some ideas about how the program works. After entering, you can play the game yourself.
Copyright and module import
1. # Memory Puzzle 2. # By Al Sweigart al@inventwithpython.com 3. # http://inventwithpython.com/pygame 4. # Released under a "Simplified BSD" license 5. 6. import random, pygame, sys 7. from pygame.locals import *
At the top of the program are comments on what the game is, who made it, and where users can find more information. Another note is that the source code can be copied freely under the "simplified BSD" license. The simplified BSD license is more applicable to software than the Creative Common license (under which this book is distributed), but they have the same basic meaning: people can copy and share the game freely. For more information about licenses, visit http://invpy.com/licenses .
The program makes use of many functions in other modules, so it imports these modules on line 6. Line 7 is also an import statement in the form of from (module name) import *, which means you don't have to type the module name in front of it. pygame. There are no functions in the locals module, but there are several constant variables we want to use, such as mousemotion, KEYUP, or QUIT. Using this style of import statement, we only need to enter mousemotion instead of pyGame locals. MOUSEMOTION.
Magic numbers are bad
9. FPS = 30 #Frames per second, the general speed of the program 10. WINDOWWIDTH = 640 # The size of the window width in pixels 11. WINDOWHEIGHT = 480 # The size of the window height in pixels 12. REVEALSPEED = 8 # Sliding display and coverage of speed box 13. BOXSIZE = 40 # The size of the box height and width in pixels 14. GAPSIZE = 10 # The size of the gap between frames in pixels
The game program in this book uses many constant variables. You may not realize why they are so convenient. For example, instead of using the BOXSIZE variable in our code, we can enter the integer 40 directly in our code. But there are two reasons for using constant variables.
First, if we want to change the size of each box in the future, we must traverse the whole program and find and replace each time we enter 40. By using only the BOXSIZE constant, we only need to change line 13, and the rest of the program is already up-to-date. This is much better, especially because we may use the integer value 40 for anything other than the white box size, and accidentally changing 40 will lead to errors in our program.
Second, it makes the code more readable. Go to the next section and look at line 18. This sets the calculation of the XMARGIN constant, that is, how many pixels are there on one side of the entire board. This is a complicated expression, but you can find out its meaning carefully. Line 18 is as follows:
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2)
But if line 18 does not use constant variables, it looks like this:
XMARGIN = int((640 – (10 * (40 + 10))) / 2)
It is now impossible to remember what the programmer's intention is. These unexplained numbers in the source code are usually called magic numbers. Whenever you find yourself entering magic numbers, you should consider replacing them with constant variables. For the Python interpreter, the first two lines are exactly the same. But for human programmers who are reading the source code and trying to understand how it works, the second version of line 18 doesn't make much sense at all! Constants do contribute to the readability of the source code.
Using assert statements for sanity checks
15. BOARDWIDTH = 10 # Number of columns for Icon 16. BOARDHEIGHT = 7 # Number of rows of Icon 17. assert (BOARDWIDTH * BOARDHEIGHT) % 2 == 0, 'Board You need an even number of boxes to match. 18. XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * (BOXSIZE + GAPSIZE))) / 2) 19. YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * (BOXSIZE + GAPSIZE))) / 2)
The assert statement on line 17 ensures that the width and height of the chessboard we choose will produce an even number of boxes (because we will have pairs of icons in this game). The assert statement consists of three parts: the assert keyword and an expression. If it is False, the program will crash. The third part (after the comma after the expression) is a string that will appear if the program crashes because of an assertion.
An assert statement with an expression basically says, "the programmer asserts that the expression must be True, or the program will crash." This is a good way to add sanity checks to your program to ensure that if execution passes assertions, we can at least know that the code works as expected.
Judge whether a number is even or odd
If the product of the board width and height is divided by 2 and the remainder is 0 (what is the remainder evaluated by the% modulus operator), the number is even. The remainder of an even number divided by 2 is always zero. The remainder of an odd number divided by 2 is always 1. If you need your code to determine whether a number is even or odd, this is a good skill to remember:
\>>> isEven = someNumber % 2 == 0
\>>> isOdd = someNumber % 2 != 0
In the above case, isEven will be True if the integer in someNumber is even. If it is an odd number, isOdd will be True.
Collapse early, often!
It's a bad thing to crash your program. This happens when your program has an error in the code and cannot continue. However, in some cases, making the program crash as soon as possible can avoid more serious errors in the future.
If the values we select for BOARDWIDTH and BOARDHEIGHT on lines 15 and 16 cause the chessboard to have an odd number of boxes (for example, if the width is 3 and the height is 5), there will always be an unpaired icon left. This will lead to errors later in the program, and it may take a lot of debugging to determine that the real source of the error is at the beginning of the program. In fact, just for fun, try annotating the assertion so that it doesn't run, and then set both the BOARDWIDTH and boardweight constants to odd numbers. When you run the program, it will immediately appear in memorypuzzle Py, which is in the getRandomizedBoard() function!
Traceback (most recent call last): File "C:\book2svn\src\memorypuzzle.py", line 292, in <module> main() File "C:\book2svn\src\memorypuzzle.py", line 58, in main mainBoard = getRandomizedBoard() File "C:\book2svn\src\memorypuzzle.py", line 149, in getRandomizedBoard columns.append(icons[0]) IndexError: list index out of range
We can spend a lot of time looking at getRandomizedBoard() to find out what's wrong with it, and then realize that getRandomizedBoard() is very good: the real source of the error is in lines 15 and 16. We set the BOARDWIDTH and boardweight constants
Assertions ensure that this will never happen. If our code is about to crash, we want it to crash immediately when it detects some serious errors, otherwise the errors may not become apparent until later in the program. Collapse early!
Whenever there are some conditions in your program that must always be True, you want to add an assert statement. Often collapse! You don't have to go too far and put assertion statements anywhere. Often crashing because of assertions is very helpful to detect the real source of errors.
Make the source code look beautiful
21. # R G B 22. GRAY = (100, 100, 100) 23. NAVYBLUE = ( 60, 60, 100) 24. WHITE = (255, 255, 255) 25. RED = (255, 0, 0) 26. GREEN = ( 0, 255, 0) 27. BLUE = ( 0, 0, 255) 28. YELLOW = (255, 255, 0) 29. ORANGE = (255, 128, 0) 30. PURPLE = (255, 0, 255) 31. CYAN = ( 0, 255, 255) 32. 33. BGCOLOR = NAVYBLUE 34. LIGHTBGCOLOR = GRAY 35. BOXCOLOR = WHITE 36. HIGHLIGHTCOLOR = BLUE
Remember that colors in Pygame are represented by tuples of three integers between 0 and 255. These three integers represent the number of red, green and blue in the color, which is why these tuples are called RGB values. Note that the spacing of tuples in lines 22 to 31 causes R, G, and B integers to line up. In Python, indentation (that is, the space at the beginning of a line) needs to be precise, but the spacing of the rest of the line is not so strict. By separating integers in tuples, we can clearly see how RGB values compare with each other. (for more information on spacing and indentation, see http://invpy.com/whitespace . )
It's a good thing to make your code more readable in this way, but don't spend too much time doing it. Code doesn't have to be beautiful to work. At some point, you will spend more time entering spaces than you save by having readable tuple values.
Use constant variables instead of strings
38. DONUT = 'donut' 39. SQUARE = 'square' 40. DIAMOND = 'diamond' 41. LINES = 'lines' 42. OVAL = 'oval'
The program also sets constant variables for some strings. These constants will be used in the data structure of the board to track which spaces on the board have which icons. It's a good idea to use constant variables instead of string values. Look at the following code, which comes from line 187:
if shape == DONUT:
The shape variable will be set to one of the strings' DONUT ',' square ',' diamond ',' lines' or 'oval', and then compared with the DONUT constant. For example, if we type the wrong word when writing line 187, as follows:
if shape == DNUOT:
Python then crashes with an error message indicating that there is no variable named DUNOT. That's ok. Since the program crashed on line 187, when we checked the line, it was easy to see that the error was caused by a spelling error. However, if we use strings instead of constant variables and enter the same spelling error, line 187 looks like this:
if shape == 'dunot':
This is perfectly acceptable Python code, so when you run it, it won't crash in the first place. However, this will lead to strange errors in our program later. Because the code doesn't crash immediately where it causes the problem, it can be much more difficult to find it.
Make sure we have enough icons
44. ALLCOLORS = (RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN) 45. ALLSHAPES = (DONUT, SQUARE, DIAMOND, LINES, OVAL) 46. assert len(ALLCOLORS) * len(ALLSHAPES) * 2 >= BOARDWIDTH * BOARDHEIGHT, "For defined shapes/The board is too big for color and quantity."
In order for our game program to create icons with various possible color and shape combinations, we need to create a tuple containing all these values. There is another assertion on line 46 to ensure that there are enough color / shape combinations to meet the size of the board we have. If not, the program will crash on line 46 and we will know that we must either add more colors and shapes or make the board smaller in width and height. Using 7 colors and 5 shapes, we can make 35 (i.e. 7 x 5) different icons. And because each icon has a pair, this means that we can have up to 70 boards (i.e. 35 x 2 or 7 x 5 x 2) space.
Tuple vs. list, immutable vs. variable
You may have noticed that the ALLCOLORS and ALLSHAPES variables are tuples rather than lists. When do we use tuples and lists? Anyway, what's the difference between them?
Tuples and lists are the same in all respects, except for two points: tuples use parentheses instead of square brackets, and items in tuples cannot be modified (but items in lists can be modified). We often call lists mutable (meaning they can be changed) and tuples immutable (meaning they cannot be changed).
For an example of trying to change values in lists and tuples, review the following code:
>>> listVal = [1, 1, 2, 3, 5, 8] >>> tupleVal = (1, 1, 2, 3, 5, 8) >>> listVal[4] = 'hello!' >>> listVal [1, 1, 2, 3, 'hello!', 8] >>> tupleVal[4] = 'hello!' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> tupleVal (1, 1, 2, 3, 5, 8) >>> tupleVal[4] 5
Please note that when we try to change the item at index 2 in the tuple, Python will give us an error message indicating that the tuple object does not support "item allocation".
The invariance of tuples has a stupid advantage and an important advantage. The benefit of stupidity is that code that uses tuples is slightly faster than code that uses lists. (Python can do some optimization because it knows that the values in tuples will never change.) But it's not important to make the code run a few nanoseconds faster.
The important benefits of using tuples are similar to those of using constant variables: it indicates that the values in tuples will never change, so anyone who reads the code later can say, "I can expect this tuple to always be the same. Otherwise, programmers will use a list." This also makes future programmers say when reading your code: "if I see a list value, I know it can be modified at some time in this program. Otherwise, the programmer who wrote this code will use tuples."
You can still assign a new tuple value to the variable:
\>>> tupleVal = (1, 2, 3)
\>>> tupleVal = (1, 2, 3, 4)
The reason this code is valid is that the code in the second line does not change the (1, 2, 3) tuple. It assigns a tuple Val of a completely new tuple (1, 2, 3, 4) and overwrites the old tuple value. However, you cannot use square brackets to modify items in tuples.
String is also an immutable data type. You can read a single character in a string using square brackets, but you cannot change a single character in the string:
\>>> strVal = 'hello' \>>> strVal[1] 'e' \>>> strVal[1] = 'X' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
An item tuple requires a trailing comma
In addition, a small detail about tuples: if you need to write code about tuples with one value, it needs to include a trailing comma, for example:
oneValueTuple = (42, )
If you forget the comma (and it's easy to forget), Python won't be able to distinguish it from a set of parentheses that just change the order of operations. For example, look at the following two lines of code:
variableA = (5 * 6) variableB = (5 * 6, )
The value stored in variableA is only the integer 30. However, the expression of the assignment statement of variableB is a single tuple value (30,). Empty tuple values do not require commas, they can themselves be a set of parentheses: ().
Convert between list and tuple
You can convert between a list and a tuple value, just as you can convert between a string and an integer value. Simply pass the tuple value to the list() function, which returns the list form of the tuple value. Alternatively, pass the list value to the tuple() function, which returns the tuple form of the list value. Try typing the following in an interactive shell:
>>> spam = (1, 2, 3, 4) >>> spam = list(spam) >>> spam [1, 2, 3, 4] >>> spam = tuple(spam) >>> spam (1, 2, 3, 4) >>>
Global declaration, why are global variables evil
48. def main(): 49. global FPSCLOCK, DISPLAYSURF 50. pygame.init() 51. FPSCLOCK = pygame.time.Clock() 52. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) 53. 54. mousex = 0 # used to store x coordinate of mouse event 55. mousey = 0 # used to store y coordinate of mouse event 56. pygame.display.set_caption('Memory Game')
This is the beginning of the main() function, where the main part of the game code is located. Later in this chapter, we will explain the functions invoked in the main() function.
Line 49 is a global statement. A global statement is a list of global keywords followed by comma separated variable names. Mark these variable names as global variables. Inside the main() function, these names do not apply to local variables that may have exactly the same name as global variables. They are global variables. Any values assigned to them in the main() function will remain outside the main() function. We mark the fpslock and DISPLAYSURF variables as global variables because they are used in several other functions in the program. (for more information, see http://invpy.com/scope . )
There are four simple rules to determine whether a variable is local or global:
-
If there is a variable at the beginning of the function global Statement, then this variable is global.
-
If a variable name in a function has the same name as a global variable and the function never assigns a value to the variable, the variable is a global variable.
-
If the name of a variable in a function is the same as that of a global variable, and the function does assign a value to the variable, the variable is a local variable.
-
If there is no global variable with the same name as the variable in the function, the variable is obviously a local variable.
You should generally avoid using global variables within functions
Functions should be like applets in a program, with specific inputs (parameters) and outputs (return values). However, functions that read and write to global variables have additional inputs and outputs. Because the global variable may have been modified in many places before calling the function, tracking errors involving the wrong values set in the global variable can be tricky.
Using a function as a separate applet that does not use global variables makes it easier to find errors in the code because the parameters of the function are explicitly known. It also makes it easier to change the code in the function, because if the new function uses the same parameters and gives the same return value, it will automatically work with the rest of the program like the old function.
Basically, using global variables may make it easier to write programs, but they usually make debugging more difficult.
In the game of this book, global variables are mainly used for global constants that will never change, but you need to call pyGame Variable of init() function. Since this happens in the main () function, they are in the set main () function, and just let other functions read. Global variables are used as constants and do not change, so they are unlikely to cause confusing errors.
If you don't understand this, please don't worry. Instead of having the function read global variables as a general rule, just write code to pass values to the function.
Data structure and 2D list
58. mainBoard = getRandomizedBoard() 59. revealedBoxes = generateRevealedBoxesData(False)
The getRandomizedBoard() function returns a data structure representing the status of the board. The generateRevealedBoxesData() function returns the data structure representing the box. The return values of these functions are two-dimensional (2D) lists or lists. The list of values will be a 3D list. Another word for a two-dimensional or multi-dimensional list is a multi-dimensional list.
If we have a list value of a variable named spam, we can use square brackets to access the value in the list, such as spam[2] to retrieve the third value in the list. If the value in spam[2] is itself a list, we can use another set of square brackets to retrieve the value in the list. For example, this looks like spam[2][4], which retrieves the fifth of the third values of the list spam. Using this representation of the list, you can easily map 2D boards to 2D list values. Since the motherboard variable will store icons in it, if we want to get the icons at (4, 5) on the board, we can use the expression mainBoard[4][5]. Since the icon itself is stored as a binomial group with shape and color, the complete data structure is a list of binomial tuples. WOW!
This is a small example. It looks like this:
The corresponding data structure is:
mainBoard = [[(DONUT, BLUE), (LINES, BLUE), (SQUARE, ORANGE)], [(SQUARE, GREEN), (DONUT, BLUE), (DIAMOND, YELLOW)], [(SQUARE, GREEN), (OVAL, YELLOW), (SQUARE, ORANGE)], [(DIAMOND, YELLOW), (LINES, BLUE), (OVAL, YELLOW)]]
If your book is black and white, you can http://invpy.com/memoryboard Up See the color version above.) You will notice that mainBoard[x][y] will correspond to the (x, y) coordinates located on the board.
At the same time, the "retrieved boxes" data structure is also a two-dimensional list. Except that it is not a binary tuple like the board data structure, it has Boolean values: if the box at the x and y coordinates is exposed, it is true; if so, it is False and masked. Passing False to the generateRevealedBoxesData() function sets all Boolean values to False. (this function will be described in detail later.)
These two data structures are used to track the state of the game board.
Start game animation
61. firstSelection = None # stores the (x, y) of the first box clicked. 62. 63. DISPLAYSURF.fill(BGCOLOR) 64. startGameAnimation(mainBoard)
Line 61 sets a variable named firstSelection with a value of none. (none is a value indicating a missing value. It is the only value for the data type NoneType. For more information, visit http://invpy.com/None )When a player clicks an icon on the board, the program needs to track whether this is the first icon or the second icon of the paired icon clicked. If firstSelection is none, click Yes. On the first icon, we store the XY coordinates in the firstSelection variable as a tuple of two integers (one for the X value and the other for the Y value). On the second click, the value will be this tuple instead of none, which is how the program tracks it as the second icon click. Line 63 fills the entire surface with the background color. It will also draw anything on the surface in the past, which provides us with a clean slate to start drawing graphics.
If you have played Memory Puzzle, you will notice that at the beginning of the game, all boxes will be quickly covered and opened randomly, so that players can have a preview and see which icons are under which boxes. All this happens in the startGameAnimation() function, which will be explained later in this chapter.
It's important to let players see it first (but don't let players easily remember the location of icons), otherwise they won't know where any icons are. Clicking on an icon blindly is not as interesting as having a hint.
Game cycle
66. while True: # main game loop 67. mouseClicked = False 68. 69. DISPLAYSURF.fill(BGCOLOR) # drawing the window 70. drawBoard(mainBoard, revealedBoxes)
The game loop is an infinite loop. Starting from line 66, it will iterate as long as the game is in progress. Remember that the game loop handles events, updates the game state, and draws the game state to the screen.
The game state of the Memory Puzzle program is stored in the following variables:
· mainBoard
· revealedBoxes
· firstSelection
· mouseClicked
· mousex
· mousey
In the Memory Puzzle program, the mouseClicked variable stores a Boolean value in each iteration of the game loop, which is True if the player clicks the mouse during this iteration of the game loop. (this is part of tracking game status.)
On line 69, the surface is painted with a background color to erase anything previously drawn on it. Then the program calls drawBoard() to draw the current state of the board according to the board and the "display box" data structure we passed to it. (these lines of code are part of drawing and updating the screen.)
Remember that our drawing function draws only on the display Surface object in memory. After we call pyGame display. Before update (), the Surface object will not actually appear on the screen, which is completed at the end of the game cycle on line 121.
Event handling loop
72. for event in pygame.event.get(): # Event handling loop 73. if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE): 74. pygame.quit() 75. sys.exit() 76. elif event.type == MOUSEMOTION: 77. mousex, mousey = event.pos 78. elif event.type == MOUSEBUTTONUP: 79. mousex, mousey = event.pos 80. mouseClicked = True
The for loop on line 72 executes code for each event that has occurred since the last iteration of the game loop. This loop is called the event handling loop (it is different from the game loop, although the event handling loop is inside the game loop), which traverses and calls pyGame event. PyGame returned by get() Event object list
If the event object is a QUIT event or a KEYUP event of the Esc key, the program should terminate. Otherwise, in the event of MOUSEMOTION event (i.e. the mouse cursor has moved) or MOUSEBUTTONUP event (i.e. the mouse button was previously pressed and now the button is released), the position of the mouse cursor should be stored in the mousex and mousey variables. mouseClicked should also be set to True if this is a MOUSEBUTTONUP event.
Once we have processed all the events, the values stored in mousex, mousey, and mouseClicked will tell us any input the player gives us. Now we should update the game status and draw the results on the screen.
Check which box the mouse cursor is in
82. boxx, boxy = getBoxAtPixel(mousex, mousey) 83. if boxx != None and boxy != None: 84. # The mouse is currently over a box. 85. if not revealedBoxes[boxx][boxy]: 86. drawHighlightBox(boxx, boxy)
The getBoxAtPixel() function returns a tuple of two integers. An integer represents the XY board coordinates of the box where the mouse coordinates are located. Explain later how getBoxAtPixel () does this. All we need to know now is that if the mousex and mousey coordinates are on a box, the function returns an XY board coordinate tuple and stores it in boxx and boxy. If the mouse cursor is not on any box (for example, if it is on one side of the board or in the gap between boxes), the function returns a tuple (None, None) and is stored in boxx and boxy.
We are only interested in the absence of None in boxx and boxy, so the next few lines of code are located in the block after the if statement on line 83 to check this situation. If executed within this block, we know that the user places the mouse cursor over a box (and may also click the mouse, depending on the value stored in mouseClicked).
The if statement at line 85 checks whether the box is overwritten by reading the value stored in revelboxes [boxx] [boxy]. If it is False, then we know that the box is covered. Whenever the mouse hovers over a covered box, we want to draw a blue highlight around the box to inform players that they can click it. This highlighting is not performed for boxes that have not been covered. Highlight drawing is handled by our drawHighlightBox() function, which will be explained later.
87. if not revealedBoxes[boxx][boxy] and mouseClicked: 88. revealBoxesAnimation(mainBoard, [(boxx, boxy)]) 89. revealedBoxes[boxx][boxy] = True # set the box as "revealed"
In line 87, we check whether the mouse cursor is not only on the covered box, but also whether the mouse has been clicked. In this case, we want to play the "show" animation of the box by calling our revealBoxesAnimation() function (like all other function main() calls, which will be explained later in this chapter). You should note that calling this function will only animate the opened box. The data structure for tracking game status is not updated until we set revealBoxes[boxx][boxy] = True in line 89.
If you comment out line 89 and run the program, you will notice that the display animation is played after clicking a box, but the box is immediately overwritten again. This is because revelboxes [boxx] [boxy] is still set to False, so in the next iteration of the game cycle, the box will be covered when drawing the board. No line 89 would cause quite a strange error in our program.
Process the first clicked box
90. if firstSelection == None: # The current box is the first box to be clicked 91. firstSelection = (boxx, boxy) 92. else: # The current box is the second clicked box 93. # Check whether the two icons match. 94. icon1shape, icon1color = getShapeAndColor(mainBoard, firstSelection[0], firstSelection[1]) 95. icon2shape, icon2color = getShapeAndColor(mainBoard, boxx, boxy)
The firstSelection variable is set to None before the execution enters the game loop. This means that no box was clicked, so if the condition on line 90 is True, it means that this is the first of the two possible matching boxes clicked. We want to play the display animation of the box and keep the box uncovered. We also set the firstSelection variable to the box coordinate tuple of the clicked box.
If this is the second box that the player clicks, we want to play the display animation of the box, and then check whether the two icons under the box match. The getShapeAndColor() function retrieves the shape and color values of the icon. (these values will be one of the values in the ALLCOLORS and ALLSHAPES tuples.)
Handle a pair of mismatched icons
97. if icon1shape != icon2shape or icon1color != icon2color: 98. # Icons don't match. Re-cover up both selections. 99. pygame.time.wait(1000) # 1000 milliseconds = 1 sec 100. coverBoxesAnimation(mainBoard, [(firstSelection[0], firstSelection[1]), (boxx, boxy)]) 101. revealedBoxes[firstSelection[0]][firstSelection [1]] = False 102. revealedBoxes[boxx][boxy] = False
The if statement at line 97 checks whether the shapes or colors of the two icons do not match. If this is the case, we want to call pyGame time. Wait (1000) pauses the game for 1000 milliseconds (i.e. 1 second) so that the player has a chance to see that the two icons do not match. Then play the "cover up" animation for the two boxes. We also want to update the game status to mark these boxes as not displayed (i.e. covered).
Handle if the player wins
103. elif hasWon(revealedBoxes): # check if all pairs found 104. gameWonAnimation(mainBoard) 105. pygame.time.wait(2000) 106. 107. # Reset the board 108. mainBoard = getRandomizedBoard() 109. revealedBoxes = generateRevealedBoxesData(False) 110. 111. # Show the fully unrevealed board for a second. 112. drawBoard(mainBoard, revealedBoxes) 113. pygame.display.update() 114. pygame.time.wait(1000) 115. 116. # Replay the start game animation. 117. startGameAnimation(mainBoard) 118. firstSelection = None # reset firstSelection variable
Otherwise, if the condition on line 97 is False, the two icons must match. At this point, the program doesn't actually need to do anything else with the box: it can keep both boxes displayed. However, the program should check whether this is the last pair of icons on the board to match. This is done in our hasWon() function. If the chessboard is in the winning state (that is, all boxes are displayed), it will return True.
If so, we want to call gameWonAnimation () to play "game win" animation, then stop a little to intoxicate their winning players, and then reset the data structure motherboard and revealedBoxes to start a new game.
Line 117 plays the "start game" animation again. After that, the program execution will cycle the game cycle as usual, and players can continue to play until they exit the program.
Whether the two boxes match or not, after the second box is clicked, line 118 will set the firstSelection variable back to None so that the next box clicked by the player will be interpreted as a pair of icons of the first click box that may match.
Draw the game state to the screen
120. # Redraw the screen and wait for the clock to tick. 121. pygame.display.update() 122. FPSCLOCK.tick(FPS)
At this time, the game state has been updated according to the player's input, and the latest game state has been drawn into the Surface object DISPLAYSURF. We have reached the end of the game loop, so we call pyGame display. Update() draws the DISPLAYSURF Surface object onto the computer screen.
Line 9 sets the FPS constant to an integer value of 30, which means we want the game to run (at most) at 30 frames per second. If we want the program to run faster, we can increase this number. If we want the program to run slower, we can reduce this number. It can even be set to a floating-point value like 0.5, which will run the program at half a frame per second, that is, one frame every two seconds.
In order to run at 30 frames per second, each frame must be drawn in 1 / 30 second. This means pyGame display. Update() and all code in the game loop must be executed within 33.3 milliseconds. Any modern computer can do this easily with a lot of time left. To prevent the program running too fast, we called pygame. in FPSCLOCK. The tick() method of the clock object and let it pause the program for the remaining 33.3 milliseconds.
Since this is done at the end of the game loop, it ensures that each iteration of the game loop takes (at least) 33.3 milliseconds. If for some reason pyGame display. If the update () call and the code in the game loop take more than 33.3 milliseconds, the tick() method will not wait at all and return immediately.
I've been saying that other functions will be explained later in this chapter. Now that we've seen the main() function and you've got an idea of how a normal program works, let's dig into the details of all the other functions called from main().
Create a display box data structure
125. def generateRevealedBoxesData(val): 126. revealedBoxes = [] 127. for i in range(BOARDWIDTH): 128. revealedBoxes.append([val] * BOARDHEIGHT) 129. return revealedBoxes
The generateRevealedBoxesData() function needs to create a list of Boolean values. The Boolean value will only be the value passed to the function as a val parameter. We start with the data structure revealBoxes variable as an empty list.
In order for the data structure to have a revealBoxes[x][y] structure, we need to ensure that the internal list represents the vertical columns of the chessboard rather than the horizontal rows. Otherwise, the data structure will have a reveledboxes [y] [x] structure.
This loop creates columns and then adds them to revealedBoxes. Columns are created using list replication, so the column list has as many val values as indicated by boardright.
Create panel data structure: Step 1 – get all possible icons
132. def getRandomizedBoard(): 133. # Get a list of every possible shape in every possible color. 134. icons = [] 135. for color in ALLCOLORS: 136. for shape in ALLSHAPES: 137. icons.append( (shape, color) )
Board data structure is a list of tuples, in which each tuple has two values: one is the shape of the icon and the other is the color of the icon. But creating this data structure is a little complicated. We need to ensure that there are exactly the same number of icons as the boxes on the board, and that there are only two and only two icons for each type.
The first step in doing this is to create a list of possible shape and color combinations. Recall that we have a list of each color and shape in ALLCOLORS and ALLSHAPES, so the nested for loop on lines 135 and 136 will traverse each possible shape of each possible color. These are added to the list of icon variables on line 137.
Step 2 – reorganize and truncate the list of all icons
139. random.shuffle(icons) # randomize the order of the icons list 140. numIconsUsed = int(BOARDWIDTH * BOARDHEIGHT / 2) # calculate how many icons are needed 141. icons = icons[:numIconsUsed] * 2 # make two of each 142. random.shuffle(icons)
But remember, there may be more possible combinations than the spaces on the chessboard. We need to multiply BOARDWIDTH by BOARDHEIGHT to calculate the number of spaces on the board. Then we divide the number by 2 because we will have pairs of icons. On a board with 70 spaces, we only need 35 different icons, because each icon will have two. This number will be stored in numIconsUsed.
Line 141 uses the list slice to get the first numIconsUsed number of icons in the list. (if you have forgotten how list slicing works, check out http://invpy.com/slicing . ) The list has been rearranged on line 139, so the icons for each game are not always the same. Then use the * operator to copy the list so that each icon has two. This new double list will overwrite the old list in the icon variable. Since the first half of the new list is the same as the second half, we call the shuffle() method again to randomly mix the order of icons.
Step 3 – place the icon on the board
144. # Create the board data structure, with randomly placed icons. 145. board = [] 146. for x in range(BOARDWIDTH): 147. column = [] 148. for y in range(BOARDHEIGHT): 149. column.append(icons[0]) 150. del icons[0] # remove the icons as we assign them 151. board.append(column) 152. return board
Now we need to create a list data structure list for the circuit board. We can do this using nested for loops, as the generateRevealedBoxesData() function does. For each column on the board, we will create a list of randomly selected icons. Just as you add icons to the column at line 149, they are removed column by column from the icon list at line 150. In this way, as the list of icons becomes shorter and shorter, icons[0] will have different icons to add to the column.
To better illustrate this, type the following code in the interactive shell. Notice how the del statement changes the myList list.
>>> myList = ['cat', 'dog', 'mouse', 'lizard'] >>> del myList[0] >>> myList ['dog', 'mouse', 'lizard'] >>> del myList[0] >>> myList ['mouse', 'lizard'] >>> del myList[0] >>> myList ['lizard'] >>> del myList[0] >>> myList []
Because we want to delete the item in front of the list, the other items move forward so that the next item in the list becomes the new "first" item. This works the same way as line 150.
Split list into lists
155. def splitIntoGroupsOf(groupSize, theList): 156. # splits a list into a list of lists, where the inner lists have at 157. # most groupSize number of items. 158. result = [] 159. for i in range(0, len(theList), groupSize): 160. result.append(theList[i:i + groupSize]) 161. return result
The splitIntoGroupsOf() function (which will be called by the function called startGameAnimation()) splits the list and puts it into a new list as a list item, where the internal list has a list of quantitative elements of groupsize. (if the remaining items are less than groupsize, the last list will be less.)
The range() function with three arguments is called on line 159. (if you are not familiar with this format, please check http://invpy.com/range . ) Let's use an example to illustrate that if the length of the list is 20 and the groupSize parameter is 8, the calculation result of range (0, len (the list), groupSize) is range(0, 20, 8). For the three iterations of the for loop, this provides values 0, 8, and 16 for the i variable.
In line 160, slicing the list with theList[i:i + groupSize] creates a list added to the result list. In each iteration where i is 0, 8, and 16 (and groupSize is 8), the list slice expression will be theList[0:8], then theList[8:16] in the second iteration, and then thelist [16:24] in the third iteration.
Note that even if the maximum index of theList is 19 in our example, even if 24 is greater than 19, theList[16:24] will not raise an IndexError error. It only creates a list slice that contains the rest of the items in the list. List slicing does not destroy or change the original list stored in theList. It just copies part of it to evaluate the new list value. This new list value is the list attached to the list in the result variable on line 160. Therefore, when we return the result at the end of this function, we are returning a list of lists.
Different coordinate systems
164. def leftTopCoordsOfBox(boxx, boxy): 165. # Convert board coordinates to pixel coordinates 166. left = boxx * (BOXSIZE + GAPSIZE) + XMARGIN 167. top = boxy * (BOXSIZE + GAPSIZE) + YMARGIN 168. return (left, top)
You should be familiar with Cartesian coordinate systems. (if you want to review this topic, please read http://invpy.com/coordinates . ) In most of our games, we will use multiple Cartesian coordinate systems. A coordinate system used in Memory Puzzle games is pixel or screen coordinates. But we will also use another coordinate system for the box. This is because it will be easier to use (3, 2) to refer to the 4th from the left box and the 3rd from the top (remember, the number starts from 0, not 1), rather than the upper left corner of the pixel coordinate box used (220, 165). However, we need a way to convert between these two coordinate systems.
This is the picture of the game and two different coordinate systems. Remember, the window is 640 pixels wide and 480 pixels high, so (639, 479) is the lower right corner (because the pixels in the upper left corner are (0, 0), not (1, 1)).
The · leftTopCoordsOfBox() · function takes box coordinates and pixel coordinates. Because a box occupies multiple pixels on the screen, we will always return a single pixel in the upper left corner of the box. The value is returned as two integer tuples. When we need to draw the pixel coordinates of these boxes, we often call leftTopCoordsOfBox().
Convert from pixel coordinates to frame coordinates
171. def getBoxAtPixel(x, y): 172. for boxx in range(BOARDWIDTH): 173. for boxy in range(BOARDHEIGHT): 174. left, top = leftTopCoordsOfBox(boxx, boxy) 175. boxRect = pygame.Rect(left, top, BOXSIZE, BOXSIZE) 176. if boxRect.collidepoint(x, y): 177. return (boxx, boxy) 178. return (None, None)
We also need a function to convert pixel coordinates (coordinates used for mouse click and mouse movement events) to box coordinates (so that we can find out which box the mouse event occurs on). The Rect object has a collidepoint() method. You can also pass X and Y coordinates. If the coordinates are within the area of the Rect object (i.e. collision), it will return True.
In order to find out which box the mouse coordinates are on, we will traverse the coordinates of each box and call the collidepoint() method on the Rect object with these coordinates. When colledepoint() returns True, we know that the clicked or moved box has been found, and the box coordinates will be returned. If None of them returns True, the getBoxAtPixel() function returns a value (None, None). This tuple is returned instead of simply returning None, because the caller of getBoxAtPixel() expects to return a tuple containing two values.
Draw icons and syntax sugar
181. def drawIcon(shape, color, boxx, boxy): 182. quarter = int(BOXSIZE * 0.25) # syntactic sugar 183. half = int(BOXSIZE * 0.5) # syntactic sugar 184. 185. left, top = leftTopCoordsOfBox(boxx, boxy) # get pixel coords from board coords
The drawIcon() function draws an icon (with the specified shape and color) at the coordinates specified by the parameters boxx and boxy. Each possible shape has a different set of Pygame drawing function calls, so we must have a large number of if and elif statements to distinguish them. (these statements are on lines 187 to 198.)
You can obtain the X and Y coordinates of the upper left edge of the box by calling the · leftTopCoordsOfBox() · function. The width and height of the box are set in the BOXSIZE constant. However, many shape drawing function calls also use the midpoint of the box and points on the four corners. We can calculate it and store it in the variables quarter and half. We can easily make the code int (BOXSIZE * 0.25) instead of the variable quarter. The code in this way becomes easier to read because it is obvious.
These variables are an example of syntax sugar. Syntax sugar is when we add code that can be written in another way (may use less actual code and variables), but it does make the source code easier to read. A constant variable is a syntax sugar. Precomputing a value and storing it in a variable is another type of syntax sugar. (for example, in the getRandomizedBoard() function, we can easily turn lines 140 and 141 into one line of code. But it's easier to read as two separate lines.) We don't need additional quarter and half variables, but having them makes the code easier to read. Easy to read code is easy to debug and upgrade later.
186. # Draw the shapes 187. if shape == DONUT: 188. pygame.draw.circle(DISPLAYSURF, color, (left + half, top + half), half - 5) 189. pygame.draw.circle(DISPLAYSURF, BGCOLOR, (left + half, top + half), quarter - 5) 190. elif shape == SQUARE: 191. pygame.draw.rect(DISPLAYSURF, color, (left + quarter, top + quarter, BOXSIZE - half, BOXSIZE - half)) 192. elif shape == DIAMOND: 193. pygame.draw.polygon(DISPLAYSURF, color, ((left + half, top), (left + BOXSIZE - 1, top + half), (left + half, top + BOXSIZE - 1), (left, top + half))) 194. elif shape == LINES: 195. for i in range(0, BOXSIZE, 4): 196. pygame.draw.line(DISPLAYSURF, color, (left, top + i), (left + i, top)) 197. pygame.draw.line(DISPLAYSURF, color, (left + i, top + BOXSIZE - 1), (left + BOXSIZE - 1, top + i)) 198. elif shape == OVAL: 199. pygame.draw.ellipse(DISPLAYSURF, color, (left, top + quarter, BOXSIZE, half))
Each of the donut, square, diamond, lines, and oval functions requires a different drawing primitive function call to create.
Syntax for obtaining board space icon shape and color
202. def getShapeAndColor(board, boxx, boxy): 203. # shape value for x, y spot is stored in board[x][y][0] 204. # color value for x, y spot is stored in board[x][y][1] 205. return board[boxx][boxy][0], board[boxx][boxy][1]
The getShapeAndColor() function has only one line. You might wonder why we need a function instead of just entering a line of code when we need it. The reason for this is the same as we use constant variables: it improves the readability of the code.
It's easy to figure out what code like shape, color = getShapeAndColor() does. However, if you look at code like shape, color = board[boxx][boxy][0], board[boxx][boxy][1], it will be a little difficult to understand.
Draw box cover
208. def drawBoxCovers(board, boxes, coverage): 209. # Draws boxes being covered/revealed. "boxes" is a list 210. # of two-item lists, which have the x & y spot of the box. 211. for box in boxes: 212. left, top = leftTopCoordsOfBox(box[0], box[1]) 213. pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, BOXSIZE, BOXSIZE)) 214. shape, color = getShapeAndColor(board, box[0], box[1]) 215. drawIcon(shape, color, box[0], box[1]) 216. if coverage > 0: # only draw the cover if there is an coverage 217. pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, coverage, BOXSIZE)) 218. pygame.display.update() 219. FPSCLOCK.tick(FPS)
The drawBoxCovers() function has three parameters: the board data structure, the (X, Y) tuple list when drawing each box, and then the number of boxes.
Since we want to use the same drawing code for each box in the box parameter, we will use the for loop on line 211, so we execute the same code for each box in the box list. In this for loop, the code should do three things: draw the background color (covering anything drawn before), draw the icon, and then draw as many white boxes as needed. The leftTopCoordsOfBox() function returns the pixel coordinates of the upper left corner of the box. The if statement on line 216 ensures that if the number in the coverage is exactly less than 0, we will not call pyGame draw. Rect() function.
When the coverage parameter is 0, there is no coverage at all. When coverage is set to 20, a 20 pixel wide white box covers the icon. We want the maximum size of the coverage setting to be the number in BOXSIZE, where the entire icon is completely covered.
drawBoxCovers() will be invoked from a separate cycle that is different from the game cycle. Therefore, it needs to call pyGame display. Update() and fpslock Tick (FPS) to display the animation. (this does mean that in this loop, no code is running to handle any events being generated. This is good because the cover and presentation animation only take about a second.)
Process display and overlay animation
222. def revealBoxesAnimation(board, boxesToReveal): 223. # Do the "box reveal" animation. 224. for coverage in range(BOXSIZE, (-REVEALSPEED) - 1, - REVEALSPEED): 225. drawBoxCovers(board, boxesToReveal, coverage) 226. 227. 228. def coverBoxesAnimation(board, boxesToCover): 229. # Do the "box cover" animation. 230. for coverage in range(0, BOXSIZE + REVEALSPEED, REVEALSPEED): 231. drawBoxCovers(board, boxesToCover, coverage)
Remember, animation only displays different images in a short time, and together they make things on the screen look as if they are moving. revealBoxesAnimation () and coverBoxesAnimation () only need to draw different amounts of white boxes to cover the icons. We can write a function called drawBoxCovers() to do this, and then let our animation function call drawBoxCovers() for each frame of animation. As we saw in the previous section, drawboxcovers () calls pyGame display. Update() and fpslock Tick (FPS) itself.
To do this, we will set a for loop to decrease (in revealBoxesAnimation()) or increase (in coverBoxesAnimation()) the value of the conversion parameter. The value of the conversion variable will increase or decrease according to the REVEALSPEED constant. In line 12, we set this constant to 8, which means that each time drawBoxCovers() is called, the white box will be reduced / increased by 8 pixels per iteration. If we increase this number, more pixels will be drawn for each call, which means that the size of the white box will decrease / increase faster. If we set it to 1, the white box will only decrease or increase by 1 pixel each iteration, so that the whole display or overlay animation takes longer.
Think of it as climbing stairs. If you climb a staircase with each step, it takes normal time to climb the whole staircase. However, if you climb two stairs at each step (and these steps take as long as before), you can climb the whole stairs twice as fast. If you can climb eight stairs at a time, you can climb the whole stairs at eight times the speed.
Draw the entire board
234. def drawBoard(board, revealed): 235. # Draws all of the boxes in their covered or revealed state. 236. for boxx in range(BOARDWIDTH): 237. for boxy in range(BOARDHEIGHT): 238. left, top = leftTopCoordsOfBox(boxx, boxy) 239. if not revealed[boxx][boxy]: 240. # Draw a covered box. 241. pygame.draw.rect(DISPLAYSURF, BOXCOLOR, (left, top, BOXSIZE, BOXSIZE)) 242. else: 243. # Draw the (revealed) icon. 244. shape, color = getShapeAndColor(board, boxx, boxy) 245. drawIcon(shape, color, boxx, boxy)
The drawBoard() function calls drawIcon() from the box on the board. The nested for loop at lines 236 and 237 will traverse each possible X and Y coordinate of the box and will draw an icon or a white square at that position (to represent the covered box).
Highlight draw
248. def drawHighlightBox(boxx, boxy): 249. left, top = leftTopCoordsOfBox(boxx, boxy) 250. pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR, (left - 5, top - 5, BOXSIZE + 10, BOXSIZE + 10), 4)
To help players realize that they can click on a covered box to display it, we will highlight it with a blue outline around a box. This profile is created by calling pyGame draw. Rect() to create a rectangle with a width of 4 pixels.
Start game animation
253. def startGameAnimation(board): 254. # Randomly reveal the boxes 8 at a time. 255. coveredBoxes = generateRevealedBoxesData(False) 256. boxes = [] 257. for x in range(BOARDWIDTH): 258. for y in range(BOARDHEIGHT): 259. boxes.append( (x, y) ) 260. random.shuffle(boxes) 261. boxGroups = splitIntoGroupsOf(8, boxes)
The animation played at the beginning of the game provides players with a quick hint of the location of all icons. In order to make this animation, we must show and hide the grouped boxes one by one. To do this, we will first create a list of all possible spaces on the board. The nested for loop at lines 257 and 258 adds (X, Y) tuples to the list in the box variable.
We will show and mask the first eight boxes in this list, then the next eight, then the next eight, and so on. However, since the order of (X, Y) tuples in the box is the same every time, boxes in the same order are displayed. (try to comment out line 260 and run it several times to see the effect.)
In order to change the box at the beginning of each game, we will call random Shuffle() function to randomly disrupt the order of tuples in the box list. Then, when we display and overwrite the first 8 boxes in this list (and then each group of 8 boxes), it will be a random 8 boxes.
To get a list of 8 boxes, we call our splitIntoGroupsOf() function, passing 8 and box lists. The list in the list returned by the function is stored in a variable named boxGroups.
Display and override box groups
263. drawBoard(board, coveredBoxes) 264. for boxGroup in boxGroups: 265. revealBoxesAnimation(board, boxGroup) 266. coverBoxesAnimation(board, boxGroup)
First, let's draw the board. Since each value in CoveredBoxes is set to False, the call to drawBoard() will eventually draw only the covered white box. The revealBoxesAnimation () and coverBoxesAnimation () functions will draw on the space of these white boxes.
This loop will process each list in the boxGroups list. We pass this to revealBoxesAnimation(), which will perform the animation of pulling the white box open to display the following icon. Then calling coverBoxesAnimation() will enable the white box to expand the animation to overlay the icon. The for loop then moves on to the next iteration, animating the next set of eight boxes.
Game win animation
269. def gameWonAnimation(board): 270. # flash the background color when the player has won 271. coveredBoxes = generateRevealedBoxesData(True) 272. color1 = LIGHTBGCOLOR 273. color2 = BGCOLOR 274. 275. for i in range(13): 276. color1, color2 = color2, color1 # swap colors 277. DISPLAYSURF.fill(color1) 278. drawBoard(board, coveredBoxes) 279. pygame.display.update() 280. pygame.time.wait(300)
When players uncover all boxes by matching each pair on the board, we want to congratulate them by flashing the background color. In the for loop, the background will be painted with the color1 variable as the color, and then painted on the board. However, each iteration loops, alternating the values color1 and color2 at 276 lines. In this way, the program will draw the alternation between two different background colors.
Remember that this function needs to call pyGame display. Update() can really make the DISPLAYSURF surface appear on the screen.
Determine whether the player has won
283. def hasWon(revealedBoxes): 284. # Returns True if all the boxes have been revealed, otherwise False 285. for i in revealedBoxes: 286. if False in i: 287. return False # return False if any boxes are covered. 288. return True
When all icon pairs match, the player wins the game. Since the value of the "revealed" data structure has been set to True when the icons have been matched, we can find a False value through a simple loop of revealedBoxes in each space. If even a False value is in revealBoxes, we know that there are still mismatched icons on the board.
Note that because revealBoxes is a list of lists, the for loop at line 285 sets the internal list to the value of i. However, we can use the in operator to search for the False value in the entire internal list. In this way, we do not need to write additional lines of code, nor do we need to have two nested for loops like this:
for x in revealedBoxes: for y in revealedBoxes[x]: if False == revealedBoxes[x][y]: return False
Why use the main() function?
291. if __name__ == '__main__': 292. main()
Having the main() function doesn't seem to make sense, because you can put the code in the global scope at the bottom of the program, and the code will run exactly the same. However, there are two good reasons to put them in the main() function.
First, this allows you to have local variables, otherwise the local variables in the main() function must become global variables. Limiting the number of global variables is a good way to keep your code simple and easy to debug. (see "why global variables are evil" in this chapter.)
Second, it allows you to import programs so that you can call and test individual functions. If memorypuzzle The PY file is in the C: \ Python 32 folder, so you can import it from the interactive shell. Type the following to test the splitIntoGroupsOf() and getBoxAtPixel() functions to ensure that they return the correct return value:
>>> import memorypuzzle >>> memorypuzzle.splitIntoGroupsOf(3, [0,1,2,3,4,5,6,7,8,9]) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] >>> memorypuzzle.getBoxAtPixel(0, 0) (None, None) >>> memorypuzzle.getBoxAtPixel(150, 150) (1, 1)
When you import a module, all the code in it runs. If we do not have the main() function and have its code in the global scope, then we import the game automatically, which really does not allow us to call a single function in it.
That's why the code is in a separate function we call main(). Then we check the built-in Python variables__ name__ Let's see if we should call the main() function. If the program itself is running, the variable is automatically set to string 'by the Python interpreter__ main__’, 'memorypuzzle 'if importing. This is why the main() function does not run when we execute the import memorypuzzle statement in an interactive shell.
This is a convenient technique to import the program you are working on from the interactive shell and test and call once to ensure that each function returns the correct value.
Why consider readability?
Many of the suggestions in this chapter are not about how to write programs that a computer can run, but about how to write programs that programmers can read. You may not understand why this is important. After all, as long as the code is effective, who cares whether it is difficult or easy for human programmers to read?
However, it is important to realize that software is rarely isolated. When you create your own game, you rarely "finish" the program. You will always get new ideas about the game features you want to add, or find new errors in the program. Therefore, it is important that your program is readable so that you can view the code and understand it. Understanding code is the first step in changing code to add more code or fix errors.
For example, this is a confusing version of a completely unreadable Memory Puzzle program. If you enter (or from http://invpy.com/memorypuzzle_obfuscated.py Download) and run it, and you'll find that it works exactly the same as the code at the beginning of this chapter. But if there are errors in this code, it is impossible to read the code and understand what happened, let alone fix the errors.
Computers don't mind unreadable code like this. It's the same for it.
import random, pygame, sys from pygame.locals import * def hhh(): global a, b pygame.init() a = pygame.time.Clock() b = pygame.display.set_mode((640, 480)) j = 0 k = 0 pygame.display.set_caption('Memory Game') i = c() hh = d(False) h = None b.fill((60, 60, 100)) g(i) while True: e = False b.fill((60, 60, 100)) f(i, hh) for eee in pygame.event.get(): if eee.type == QUIT or (eee.type == KEYUP and eee.key == K_ESCAPE): pygame.quit() sys.exit() elif eee.type == MOUSEMOTION: j, k = eee.pos elif eee.type == MOUSEBUTTONUP: j, k = eee.pos e = True bb, ee = m(j, k) if bb != None and ee != None: if not hh[bb][ee]: n(bb, ee) if not hh[bb][ee] and e: o(i, [(bb, ee)]) hh[bb][ee] = True if h == None: h = (bb, ee) else: q, fff = s(i, h[0], h[1]) r, ggg = s(i, bb, ee) if q != r or fff != ggg: pygame.time.wait(1000) p(i, [(h[0], h[1]), (bb, ee)]) hh[h[0]][h[1]] = False hh[bb][ee] = False elif ii(hh): jj(i) pygame.time.wait(2000) i = c() hh = d(False) f(i, hh) pygame.display.update() pygame.time.wait(1000) g(i) h = None pygame.display.update() a.tick(30) def d(ccc): hh = [] for i in range(10): hh.append([ccc] * 7) return hh def c(): rr = [] for tt in ((255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 128, 0), (255, 0, 255), (0, 255, 255)): for ss in ('a', 'b', 'c', 'd', 'e'): rr.append( (ss, tt) ) random.shuffle(rr) rr = rr[:35] * 2 random.shuffle(rr) bbb = [] for x in range(10): v = [] for y in range(7): v.append(rr[0]) del rr[0] bbb.append(v) return bbb def t(vv, uu): ww = [] for i in range(0, len(uu), vv): ww.append(uu[i:i + vv]) return ww def aa(bb, ee): return (bb * 50 + 70, ee * 50 + 65) def m(x, y): for bb in range(10): for ee in range(7): oo, ddd = aa(bb, ee) aaa = pygame.Rect(oo, ddd, 40, 40) if aaa.collidepoint(x, y): return (bb, ee) return (None, None) def w(ss, tt, bb, ee): oo, ddd = aa(bb, ee) if ss == 'a': pygame.draw.circle(b, tt, (oo + 20, ddd + 20), 15) pygame.draw.circle(b, (60, 60, 100), (oo + 20, ddd + 20), 5) elif ss == 'b': pygame.draw.rect(b, tt, (oo + 10, ddd + 10, 20, 20)) elif ss == 'c': pygame.draw.polygon(b, tt, ((oo + 20, ddd), (oo + 40 - 1, ddd + 20), (oo + 20, ddd + 40 - 1), (oo, ddd + 20))) elif ss == 'd': for i in range(0, 40, 4): pygame.draw.line(b, tt, (oo, ddd + i), (oo + i, ddd)) pygame.draw.line(b, tt, (oo + i, ddd + 39), (oo + 39, ddd + i)) elif ss == 'e': pygame.draw.ellipse(b, tt, (oo, ddd + 10, 40, 20)) def s(bbb, bb, ee): return bbb[bb][ee][0], bbb[bb][ee][1] def dd(bbb, boxes, gg): for box in boxes: oo, ddd = aa(box[0], box[1]) pygame.draw.rect(b, (60, 60, 100), (oo, ddd, 40, 40)) ss, tt = s(bbb, box[0], box[1]) w(ss, tt, box[0], box[1]) if gg > 0: pygame.draw.rect(b, (255, 255, 255), (oo, ddd, gg, 40)) pygame.display.update() a.tick(30) def o(bbb, cc): for gg in range(40, (-8) - 1, -8): dd(bbb, cc, gg) def p(bbb, ff): for gg in range(0, 48, 8): dd(bbb, ff, gg) def f(bbb, pp): for bb in range(10): for ee in range(7): oo, ddd = aa(bb, ee) if not pp[bb][ee]: pygame.draw.rect(b, (255, 255, 255), (oo, ddd, 40, 40)) else: ss, tt = s(bbb, bb, ee) w(ss, tt, bb, ee) def n(bb, ee): oo, ddd = aa(bb, ee) pygame.draw.rect(b, (0, 0, 255), (oo - 5, ddd - 5, 50, 50), 4) def g(bbb): mm = d(False) boxes = [] for x in range(10): for y in range(7): boxes.append( (x, y) ) random.shuffle(boxes) kk = t(8, boxes) f(bbb, mm) for nn in kk: o(bbb, nn) p(bbb, nn) def jj(bbb): mm = d(True) tt1 = (100, 100, 100) tt2 = (60, 60, 100) for i in range(13): tt1, tt2 = tt2, tt1 b.fill(tt1) f(bbb, mm) pygame.display.update() pygame.time.wait(300) def ii(hh): for i in hh: if False in i: return False return True if __name__ == '__main__': hhh()
Never write such code. If you program like this in the mirror in the bathroom with the lights off, the ghost of Ada Lovelace will come out of the mirror and throw you into the chin of the jacquard loom.
Summary and suggestions
This chapter contains a complete description of how the Memory Puzzle program works. Read this chapter and the source code again to better understand it. Many other game programs in this book use the same programming concepts (such as nested for loops, syntax sugar and different coordinate systems in the same program), so they will not be explained for the sake of brevity.
One idea to try to understand how code works is to deliberately destroy it by commenting out random lines. Performing this operation on some lines may result in syntax errors that completely prevent the script from running. But commenting out other lines can lead to strange errors and other cool effects. Try this and find out why the program is wrong.
This is also the first step to add your own secret cheating or hacker to the program. By breaking the normal function of the program, you can learn how to change it to achieve some clever effects (such as secretly reminding you how to solve problems). Feel free to try. If you want to play the regular game again, you can always save a copy of the unchanged source code in a different file.
In fact, if you want to practice fixing errors, several versions of the game's source code have small errors. You can http://invpy.com/buggy/memorypuzzle Download these problematic versions. Try running the program to find out what the error is and why it is.