Опубликован: 06.08.2013 | Доступ: свободный | Студентов: 835 / 14 | Длительность: 27:51:00
Лекция 4:

Slide puzzle

< Лекция 3 || Лекция 4: 1234 || Лекция 5 >

"Equal To One Of" Trick with the in Operator

The expression event.key in (K_LEFT, K_a) is just a Python trick to make the code simpler. It is a way of saying "evaluate to True if event.key is equal to one of K_LEFT or K_a". The following two expressions will evaluate the exact same way:

event.key in (K_LEFT, K_a)
event.key == K_LEFT or event.key == K_a

You can really save on some space by using this trick when you have to check if a value is equal to one of multiple values. The following two expressions will evaluate the exact same way:

spam == 'dog' or spam == 'cat' or spam == 'mouse' or spam == 'horse' or spam ==
42 or spam == 'dingo'
spam in ('dog', 'cat', 'mouse', 'horse', 42, 'dingo')

WASD and Arrow Keys

The W, A, S, and D keys (together called the WASD keys, pronounced "waz-dee") are commonly used in computer games to do the same thing as the arrow keys, except the player can use their left hand instead (since the WASD keys are on the left side of the keyboard). W is for up, A is for left, S is for down, and D is for right. You can easily remember this because the WASD keys have the same layout as the arrow keys:

Рис. 4.3.

Actually Performing the Tile Slide

109.         if slideTo:
110.             slideAnimation(mainBoard, slideTo, 'Click tile or press arrow
keys to slide.', 8) # show slide on screen
111.             makeMove(mainBoard, slideTo)
112.             allMoves.append(slideTo) # record the slide
113.         pygame.display.update()
114.         FPSCLOCK.tick(FPS)

Now that the events have all been handled, we should update the variables of the game state and display the new state on the screen. If slideTo has been set (either by the mouse event or keyboard event handling code) then we can call slideAnimation() to perform the sliding animation. The parameters are the board data structure, the direction of the slide, a message to display while sliding the tile, and the speed of the sliding.

After it returns, we need to update the actual board data structure (which is done by the makeMove() function) and then add the slide to the allMoves list of all the slides made so far. This is done so that if the player clicks on the "Reset" button, we know how to undo all the player's slides.

IDLE and Terminating Pygame Programs

117. def terminate():
118.     pygame.quit()
119.     sys.exit()

This is a function that we can call that calls both the pygame.quit() and sys.exit() functions. This is a bit of syntactic sugar, so that instead of remembering to make both of these calls, there is just a single function we can call instead.

Checking for a Specific Event, and Posting Events to Pygame's Event Queue

122. def checkForQuit():
123.     for event in pygame.event.get(QUIT): # get all the QUIT events
124.         terminate() # terminate if any QUIT events are present
125.     for event in pygame.event.get(KEYUP): # get all the KEYUP events
126.         if event.key == K_ESCAPE:
127.             terminate() # terminate if the KEYUP event was for the Esc key
128.         pygame.event.post(event) # put the other KEYUP event objects back

The checkForQuit() function will check for QUIT events (or if the user has pressed the Esc key) and then call the terminate() function. But this is a bit tricky and requires some explanation.

Pygame internally has its own list data structure that it creates and appends Event objects to as they are made. This data structure is called the event queue. When the pygame.event.get() function is called with no parameters, the entire list is returned. However, you can pass a constant like QUIT to pygame.event.get() so that it will only return the QUIT events (if any) that are in the internal event queue. The rest of the events will stay in the event queue for the next time pygame.event.get() is called.

You should note that Pygame's event queue only stores up to 127 Event objects. If your program does not call pygame.event.get() frequently enough and the queue fills up, then any new events that happen won't be added to the event queue.

Line 123 pulls out a list of QUIT events from Pygame's event queue and returns them. If there are any QUIT events in the event queue, the program terminates.

Line 125 pulls out all the KEYUP events from the event queue and checks if any of them are for the Esc key. If one of the events is, then the program terminates. However, there could be KEYUP events for keys other than the Esc key. In this case, we need to put the KEYUP event back into Pygame's event queue. We can do this with the pygame.event.post() function, which adds the Event object passed to it to the end of the Pygame event queue. This way, when line 70 calls pygame.event.get() the non-Esc key KEYUP events will still be there. Otherwise calls to checkForQuit() would "consume" all of the KEYUP events and those events would never be handled.

The pygame.event.post() function is also handy if you ever want your program to add Event objects to the Pygame event queue.

Creating the Board Data Structure

131. def getStartingBoard():
132.     # Return a board data structure with tiles in the solved state.
133.     # For example, if BOARDWIDTH and BOARDHEIGHT are both 3, this function
134.     # returns [[1, 4, 7], [2, 5, 8], [3, 6, None]]
135.     counter = 1
136.     board = []
137.     for x in range(BOARDWIDTH):
138.         column = []
139.         for y in range(BOARDHEIGHT):
140.             column.append(counter)
141.             counter += BOARDWIDTH
142.         board.append(column)
143.         counter -= BOARDWIDTH * (BOARDHEIGHT - 1) + BOARDWIDTH - 1
145.     board[BOARDWIDTH-1][BOARDHEIGHT-1] = None
146.     return board

The getStartingBoard() data structure will create and return a data structure that represents a "solved" board, where all the numbered tiles are in order and the blank tile is in the lower right corner. This is done with nested for loops, just like the board data structure in the Memory Puzzle game was made.

However, notice that the first column isn't going to be [1, 2, 3] but instead [1, 4, 7]. This is because the numbers on the tiles increase by 1 going across the row, not down the column. Going down the column, the numbers increase by the size of the board's width (which is stored in the BOARDWIDTH constant). We will use the counter variable to keep track of the number that should go on the next tile. When the numbering of the tiles in the column is finished, then we need to set counter to the number at the start of the next column.

Not Tracking the Blank Position

149. def getBlankPosition(board):
150.     # Return the x and y of board coordinates of the blank space.
151.     for x in range(BOARDWIDTH)):
152.         for y in range(BOARDHEIGHT):
153.             if board[x][y] == None:
154.                 return (x, y)

Whenever our code needs to find the XY coordinates of the blank space, instead of keeping track of where the blank space is after each slide, we can just create a function that goes through the entire board and finds the blank space coordinates. The None value is used in the board data structure to represent the blank space. The code in getBlankPosition() simply uses nested for loops to find which space on the board is the blank space.

Making a Move by Updating the Board Data Structure

157. def makeMove(board, move):
158.     # This function does not check if the move is valid.
159.     blankx, blanky = getBlankPosition(board)
161.     if move == UP:
162.         board[blankx][blanky], board[blankx][blanky + 1] =
board[blankx][blanky + 1], board[blankx][blanky]
163.     elif move == DOWN:
164.         board[blankx][blanky], board[blankx][blanky - 1] =
board[blankx][blanky - 1], board[blankx][blanky]
165.     elif move == LEFT:
166.         board[blankx][blanky], board[blankx + 1][blanky] = board[blankx +
1][blanky], board[blankx][blanky]
167.     elif move == RIGHT:
168.         board[blankx][blanky], board[blankx - 1][blanky] = board[blankx -
1][blanky], board[blankx][blanky]

The data structure in the board parameter is a 2D list that represents where all the tiles are. Whenever the player makes a move, the program needs to update this data structure. What happens is that the value for the tile is swapped with the value for the blank space.

The makeMove() function doesn't have to return any values, because the board parameter has a list reference passed for its argument. This means that any changes we make to board in this function will be made to the list value that was passed to makeMove() (You can review the concept of references at http://invpy.com/references).

When NOT to Use an Assertion

171. def isValidMove(board, move):
172.     blankx, blanky = getBlankPosition(board)
173.     return (move == UP and blanky != len(board[0]) - 1) or \
174.            (move == DOWN and blanky != 0) or \
175.            (move == LEFT and blankx != len(board) - 1) or \
176.            (move == RIGHT and blankx != 0)

The isValidMove() function is passed a board data structure and a move the player would want to make. The return value is True if this move is possible and False if it is not. For example, you cannot slide a tile to the left one hundred times in a row, because eventually the blank space will be at the edge and there are no more tiles to slide to the left.

Whether a move is valid or not depends on where the blank space is. This function makes a call to getBlankPosition() to find the X and Y coordinates of the blank spot. Lines 173 to 176 are a return statement with a single expression. The \ slashes at the end of the first three lines tells the Python interpreter that that is not the end of the line of code (even though it is at the end of the line). This will let us split up a "line of code" across multiple lines to look pretty, rather than just have one very long unreadable line.

Because the parts of this expression in parentheses are joined by or operators, only one of them needs to be True for the entire expression to be True. Each of these parts checks what the intended move is and then sees if the coordinate of the blank space allows that move.

Getting a Not-So-Random Move

179. def getRandomMove(board, lastMove=None):
180.     # start with a full list of all four moves
181.     validMoves = [UP, DOWN, LEFT, RIGHT]
183.     # remove moves from the list as they are disqualified
184.     if lastMove == UP or not isValidMove(board, DOWN):
185.         validMoves.remove(DOWN)
186.     if lastMove == DOWN or not isValidMove(board, UP):
187.         validMoves.remove(UP)
188.     if lastMove == LEFT or not isValidMove(board, RIGHT):
189.         validMoves.remove(RIGHT)
190.     if lastMove == RIGHT or not isValidMove(board, LEFT):
191.         validMoves.remove(LEFT)
193.     # return a random move from the list of remaining moves
194.     return random.choice(validMoves)

At the beginning of the game, we start with the board data structure in the solved, ordered state and create the puzzle by randomly sliding around tiles. To decide which of the four directions we should slide, we'll call our getRandomMove() function. Normally we could just use the random.choice() function and pass it a tuple (UP, DOWN, LEFT, RIGHT) to have Python simply randomly choose a direction value for us. But the Sliding Puzzle game has a small restriction that prevents us from choosing a purely random number.

If you had a slide puzzle and slid a tile to left, and then slid a tile to the right, you would end up with the exact same board you had at the start. It's pointless to make a slide followed by the opposite slide. Also, if the blank space is in the lower right corner than it is impossible to slide a tile up or to the left.

The code in getRandomMove() will take these factors into account. To prevent the function from selecting the last move that was made, the caller of the function can pass a directional value for the lastMove parameter. Line 181 starts with a list of all four directional values stored in the validMoves variable. The lastMove value (if not set to None) is removed from validMoves. Depending on if the blank space is at the edge of the board, lines 184 to 191 will remove other directional values from the lastMove list.

Of the values that are left in lastMove, one of them is randomly selected with a call to random.choice() and returned.

Converting Tile Coordinates to Pixel Coordinates

197. def getLeftTopOfTile(tileX, tileY):
198.     left = XMARGIN + (tileX * TILESIZE) + (tileX - 1)
199.     top = YMARGIN + (tileY * TILESIZE) + (tileY - 1)
200.     return (left, top)

The getLeftTopOfTile() function converts board coordinates to pixel coordinates. For the board XY coordinates that are passed in, the function calculates and returns the pixel XY coordinates of the pixel at the top left of that board space.

< Лекция 3 || Лекция 4: 1234 || Лекция 5 >
Константин Шилов
Константин Шилов
Павел Лафицкий
Павел Лафицкий