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

Star Pusher

< Лекция 8 || Лекция 9: 123 || Лекция 10 >
Аннотация: Programming the game "Star Pusher".
Ключевые слова: star, box, clone, player, with, ground, tiling, Sprite, push, Top, if, pull, corner, restart, level, ALL, floor, AND, NEXT, Grid, size, CAN, form, complex, create, NOT, source coding, download, FROM, this, folder, AS, GET, error message, find, look, Line, CHECK, Copy, paste, web form, SEE, book, LIKE, EAT, MAP, State, chapter, mean, object-oriented programming, sensing, dictionary, VALUES, used, non-overlapping, screen, explain, fact, part, diagram, extra, ROCK, constant, percentage, setup, WHERE, global, separate, surface, data structure, list, character string, clear, function, explanation, Data, structure, INDEX, example, set, initial, off, First, Object, runlevel, integer, return, ONE, goal, skip, go, back, previous, Increment, decrement, Last, pass, statement, comment, python, interpretation, expectation, remove, still, reason, include, readability, string, distinction, track, while, very, Modify, original, lost, tuple, REFERENCES, assignment statement, problem, actual, guarantee, expression, BIT, bottom, row, Full, Height, camera, ITS, Position, direction, Character, keypresses, iteration, Checked, call, CASE, cause, slowdown, false, slow, drawing, rerun, I., panning, location, USER, TIME, draw, Graphics, else, point, read, blank space, change, programming, conception, recursion, recursive function, section, adjacent, Shape, space, valid, long, path, able, caller, information, desired, Modified, text, center, store, title, axis, positioning, renderer, blit, additional, pixel, gap, indicate, terminate, tick, format, lowercase letter, hard, drive, useful, HAVING, IDEA, new, text file, Windows, application, mac, text editor, idle, editor, difference, word, Font, color, text based, binary file, important, game programming, OPEN, Write, mode, interactive, shell, C, DELETE, blank, assignment, overwrite, send, filename, second, PARAMETER, result, reinstall, system, computer, launch, nuclear, method call, Add, newline character, end, newline, content, method, alternative, entire, leave, exact, file format, load, specified, EXISTS, strip, semicolon, sign, length, rectangle, PAD, column, empty list, NEST, fill, proper, error, assertion, starting point, finally, processing, GASP, function call, let, CONDITION, print, execution, execute, lines of code, program termination, local variable, Local, resume, memory, amount, output, infinite loop, prevent, running, crash, stack overflow, even, recursive call, base, perform, flood fill, algorithm, DESCRIBE, default, convert, coordinate, right, Left, tutorial, Width, Calculated, begin, background, appropriate, Basic, tree, cover, Green, scattered, design level, custom, configuration, level editor, programming practice, try, FIX

How to Play Star Pusher


Рис. 9.1.

Star Pusher is a Sokoban or "Box Pusher" clone. The player is in a room with several stars. There are star marks on the grounds of some of the tile sprites in the room. The player must figure out how to push the stars on top of the tiles with star marks. The player cannot push a star if there is a wall or another star behind it. The player cannot pull stars, so if a star gets pushed into a corner, the player will have to restart the level. When all of the stars have been pushed onto star-marked floor tiles, the level is complete and the next level starts.

Each level is made up of a 2D grid of tile images. Tile sprites are images of the same size that can be placed next to each other to form more complex images. With a few floor and wall tiles, we can create levels of many interesting shapes and sizes.

The level files are not included in the source code. Instead, you can either create the level files yourself or download them. A level file with 201 levels can be downloaded from http://invpy.com/starPusherLevels.txt. When you run the Star Pusher program, make sure that this level file is in the same folder as the starpusher.py file. Otherwise you will get this error message: AssertionError: Cannot find the level file: starPusherLevels.txt

The level designs were originally made David W. Skinner. You can download more puzzles from his website at http://users.bentonrea.com/~sasquatch/sokoban/

Source Code to Star Pusher

This source code can be downloaded from http://invpy.com/starpusher.py . If you get any error messages, look at the line number that is mentioned in the error message and check your code for any typos. You can also copy and paste your code into the web form at http://invpy.com/diff/starpusher to see if the differences between your code and the code in the book.

The level file can be downloaded from http://invpy.com/starPusherLevels.txt. The tiles can be downloaded from http://invpy.com/starPusherImages.zip.

Also, just like the squirrel, grass, and enemy "objects" in the Squirrel Eat Squirrel game, when I say "map objects", "game state objects", or "level objects" in this chapter, I do not mean objects in the Object-Oriented Programming sense. These "objects" are really just dictionary values, but it is easier to refer to them as objects since they represent things in the game world.

The Initial Setup

1. # Star Pusher (a Sokoban clone)
2. # By Al Sweigart al@inventwithpython.com
3. # http://inventwithpython.com/pygame
4. # Creative Commons BY-NC-SA 3.0 US
5.
6. import random, sys, copy, os, pygame
7. from pygame.locals import *
8.
9. FPS = 30 # frames per second to update the screen
10. WINWIDTH = 800 # width of the program's window, in pixels
11. WINHEIGHT = 600 # height in pixels
12. HALF_WINWIDTH = int(WINWIDTH / 2)
13. HALF_WINHEIGHT = int(WINHEIGHT / 2)
14.
15. # The total width and height of each tile in pixels.
16. TILEWIDTH = 50
17. TILEHEIGHT = 85
18. TILEFLOORHEIGHT = 45
19.
20. CAM_MOVE_SPEED = 5 # how many pixels per frame the camera moves
21.
22. # The percentage of outdoor tiles that have additional
23. # decoration on them, such as a tree or rock.
24. OUTSIDE_DECORATION_PCT = 20
25.
26. BRIGHTBLUE = (  0, 170, 255)
27. WHITE      = (255, 255, 255)
28. BGCOLOR = BRIGHTBLUE
29. TEXTCOLOR = WHITE
30.
31. UP = 'up'
32. DOWN = 'down'
33. LEFT = 'left'
34. RIGHT = 'right'

These constants are used in various parts of the program. The TILEWIDTH and TILEHEIGHT variables show that each of the tile images are 50 pixels wide and 85 pixels tall. However, these tiles overlap with each other when drawn on the screen (This is explained later). The TILEFLOORHEIGHT refers to the fact that the part of the tile that represents the floor is 45 pixels tall. Here is a diagram of the plain floor image:


Рис. 9.2.

The grassy tiles outside of the level’s room will sometimes have extra decorations added to them (such as trees or rocks). The OUTSIDE_DECORATION_PCT constant shows what percentage of these tiles will randomly have these decorations.

37. def main():
38.     global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING,
OUTSIDEDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage
39.
40.     # Pygame initialization and basic set up of the global variables.
41.     pygame.init()
42.     FPSCLOCK = pygame.time.Clock()
43.
44.     # Because the Surface object stored in DISPLAYSURF was returned
45.     # from the pygame.display.set_mode() function, this is the
46.     # Surface object that is drawn to the actual computer screen
47.     # when pygame.display.update() is called.
48.     DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT))
49.
50.     pygame.display.set_caption('Star Pusher')
51.     BASICFONT = pygame.font.Font('freesansbold.ttf', 18)

This is the usual Pygame setup that happens at the beginning of the program.

53.     # A global dict value that will contain all the Pygame
54.     # Surface objects returned by pygame.image.load().
55.     IMAGESDICT = {'uncovered goal': pygame.image.load('RedSelector.png'),
56.                   'covered goal': pygame.image.load('Selector.png'),
57.                   'star': pygame.image.load('Star.png'),
58.                   'corner': pygame.image.load('Wall Block Tall.png'),
59.                   'wall': pygame.image.load('Wood Block Tall.png'),
60.                   'inside floor': pygame.image.load('Plain Block.png'),
61.                   'outside floor': pygame.image.load('Grass Block.png'),
62.                   'title': pygame.image.load('star_title.png'),
63.                   'solved': pygame.image.load('star_solved.png'),
64.                   'princess': pygame.image.load('princess.png'),
65.                   'boy': pygame.image.load('boy.png'),
66.                   'catgirl': pygame.image.load('catgirl.png'),
67.                   'horngirl': pygame.image.load('horngirl.png'),
68.                   'pinkgirl': pygame.image.load('pinkgirl.png'),
69.                   'rock': pygame.image.load('Rock.png'),
70.                   'short tree': pygame.image.load('Tree_Short.png'),
71.                   'tall tree': pygame.image.load('Tree_Tall.png'),
72.                   'ugly tree': pygame.image.load('Tree_Ugly.png')}

The IMAGESDICT is a dictionary where all of the loaded images are stored. This makes it easier to use in other functions, since only the IMAGESDICT variable needs to be made global. If we stored each of these images in separate variables, then all 18 variables (for the 18 images used in this game) would need to be made global. A dictionary containing all of the Surface objects with the images is easier to handle.

74.     # These dict values are global, and map the character that appears
75.     # in the level file to the Surface object it represents.
76.     TILEMAPPING = {'x': IMAGESDICT['corner'],
77.                    '#': IMAGESDICT['wall'],
78.                    'o': IMAGESDICT['inside floor'],
79.                    ' ': IMAGESDICT['outside floor']}

The data structure for the map is just a 2D list of single character strings. The TILEMAPPING dictionary links the characters used in this map data structure to the images that they represent (This will become more clear in the drawMap() function’s explanation).

80.     OUTSIDEDECOMAPPING = {'1': IMAGESDICT['rock'],
81.                           '2': IMAGESDICT['short tree'],
82.                           '3': IMAGESDICT['tall tree'],
83.                           '4': IMAGESDICT['ugly tree']}

The OUTSIDEDECOMAPPING is also a dictionary that links the characters used in the map data structure to images that were loaded. The "outside decoration" images are drawn on top of the outdoor grassy tile.

85.     # PLAYERIMAGES is a list of all possible characters the player can be.
86.     # currentImage is the index of the player's current player image.
87.     currentImage = 0
88.     PLAYERIMAGES = [IMAGESDICT['princess'],
89.                     IMAGESDICT['boy'],
90.                     IMAGESDICT['catgirl'],
91.                     IMAGESDICT['horngirl'],
92.                     IMAGESDICT['pinkgirl']]

The PLAYERIMAGES list stores the images used for the player. The currentImage variable tracks the index of the currently selected player image. For example, when currentImage is set to 0 then PLAYERIMAGES[0], which is the "princess" player image, is drawn to the screen.

94.     startScreen() # show the title screen until the user presses a key
95.
96.     # Read in the levels from the text file. See the readLevelsFile() for
97.     # details on the format of this file and how to make your own levels.
98.     levels = readLevelsFile('starPusherLevels.txt')
99.     currentLevelIndex = 0

The startScreen() function will keep displaying the initial start screen (which also has the instructions for the game) until the player presses a key. When the player presses a key, the startScreen() function returns and then reads in the levels from the level file. The player starts off on the first level, which is the level object in the levels list at index 0.

101.     # The main game loop. This loop runs a single level, when the user
102.     # finishes that level, the next/previous level is loaded.
103.     while True: # main game loop
104.         # Run the level to actually start playing the game:
105.         result = runLevel(levels, currentLevelIndex)

The runLevel() function handles all the action for the game. It is passed a list of level objects, and the integer index of the level in that list to be played. When the player has finished playing the level, runLevel() will return one of the following strings: 'solved' (because the player has finished putting all the stars on the goals), 'next' (because the player wants to skip to the next level), 'back' (because the player wants to go back to the previous level), and 'reset' (because the player wants to start playing the current level over again, maybe because they pushed a star into a corner).

107.         if result in ('solved', 'next'):
108.             # Go to the next level.
109.             currentLevelIndex += 1
110.             if currentLevelIndex >= len(levels):
111.                 # If there are no more levels, go back to the first one.
112.                 currentLevelIndex = 0
113.         elif result == 'back':
114.             # Go to the previous level.
115.             currentLevelIndex -= 1
116.             if currentLevelIndex < 0: 
117.                 # If there are no previous levels, go to the last one. 
118.                 currentLevelIndex = len(levels)-1 

If runLevel() has returned the strings 'solved' or 'next', then we need to increment levelNum by 1. If this increments levelNum beyond the number of levels there are, then levelNum is set back at 0.

The opposite is done if 'back' is returned, then levelNum is decremented by 1. If this makes it go below 0, then it is set to the last level (which is len(levels)-1).

119.         elif result == 'reset':
120.             pass # Do nothing. Loop re-calls runLevel() to reset the level

If the return value was 'reset', then the code does nothing. The pass statement does nothing (like a comment), but is needed because the Python interpreter expects an indented line of code after an elif statement.

We could remove lines 119 and 120 from the source code entirely, and the program will still work just the same. The reason we include it here is for program readability, so that if we make changes to the code later, we won’t forget that runLevel() can also return the string 'reset'.

123. def runLevel(levels, levelNum):
124.     global currentImage
125.     levelObj = levels[levelnum]
126.     mapObj = decorateMap(levelObj['mapObj'],
levelObj['startState']['player'])
127.     gameStateObj = copy.deepcopy(levelObj['startState'])

The levels list contains all the level objects that were loaded from the level file. The level object for the current level (which is what levelNum is set to) is stored in the levelObj variable. A map object (which makes a distinction between indoor and outdoor tiles, and decorates the outdoor tiles with trees and rocks) is returned from the decorateMap() function. And to track the state of the game while the player plays this level, a copy of the game state object that is stored in levelObj is made using the copy.deepcopy() function.

The game state object copy is made because the game state object stored in levelObj['startState'] represents the game state at the very beginning of the level, and we do not want to modify this. Otherwise, if the player restarts the level, the original game state for that level will be lost.

The copy.deepcopy() function is used because the game state object is a dictionary of that has tuples. But technically, the dictionary contains references to tuples. (References are explained in detail at http://invpy.com/references .) Using an assignment statement to make a copy of the dictionary will make a copy of the references but not the values they refer to, so that both the copy and the original dictionary still refer to the same tuples.

The copy.deepcopy() function solves this problem by making copies of the actual tuples in the dictionary. This way we can guarantee that changing one dictionary will not affect the other dictionary.

128.     mapNeedsRedraw = True # set to True to call drawMap()
129.     levelSurf = BASICFONT.render('Level %s of %s' % (levelObj['levelNum']
+ 1, totalNumOfLevels), 1, TEXTCOLOR)
130.     levelRect = levelSurf.get_rect()
131.     levelRect.bottomleft = (20, WINHEIGHT - 35)
132.     mapWidth = len(mapObj) * TILEWIDTH
133.     mapHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) +
TILEHEIGHT
134.     MAX_CAM_X_PAN = abs(HALF_WINHEIGHT - int(mapHeight / 2)) + TILEWIDTH
135.     MAX_CAM_Y_PAN = abs(HALF_WINWIDTH - int(mapWidth / 2)) + TILEHEIGHT
136.
137.     levelIsComplete = False
138.     # Track how much the camera has moved:
139.     cameraOffsetX = 0
140.     cameraOffsetY = 0
141.     # Track if the keys to move the camera are being held down:
142.     cameraUp = False
143.     cameraDown = False
144.     cameraLeft = False
145.     cameraRight = False

More variables are set at the start of playing a level. The mapWidth and mapHeight variables are the size of the maps in pixels. The expression for calculating mapHeight is a bit complicated since the tiles overlap each other. Only the bottom row of tiles is the full height (which accounts for the + TILEHEIGHT part of the expression), all of the other rows of tiles (which number as (len(mapObj[0]) - 1)) are slightly overlapped. This means that they are effectively each only (TILEHEIGHT - TILEFLOORHEIGHT) pixels tall.

The camera in Star Pusher can be moved independently of the player moving around the map. This is why the camera needs its own set of "moving" variables: cameraUp, cameraDown, cameraLeft, and cameraRight. The cameraOffsetX and cameraOffsetY variables track the position of the camera.

147.     while True: # main game loop
148.         # Reset these variables:
149.         playerMoveTo = None
150.         keyPressed = False
151.
152.         for event in pygame.event.get(): # event handling loop
153.             if event.type == QUIT:
154.                 # Player clicked the "X" at the corner of the window.
155.                 terminate()

The playerMoveTo variable will be set to the direction constant that the player intends to move the player character on the map. The keyPressed variable tracks if any key has been pressed during this iteration of the game loop. This variable is checked later when the player has solved the level.

157.             elif event.type == KEYDOWN:
158.                 # Handle key presses
159.                 keyPressed = True
160.                 if event.key == K_LEFT:
161.                     playerMoveTo = LEFT
162.                 elif event.key == K_RIGHT:
163.                     playerMoveTo = RIGHT
164.                 elif event.key == K_UP:
165.                     playerMoveTo = UP
166.                 elif event.key == K_DOWN:
167.                     playerMoveTo = DOWN
168.
169.                 # Set the camera move mode.
170.                 elif event.key == K_a:
171.                     cameraLeft = True
172.                 elif event.key == K_d:
173.                     cameraRight = True
174.                 elif event.key == K_w:
175.                     cameraUp = True
176.                 elif event.key == K_s:
177.                     cameraDown = True
178.
179.                 elif event.key == K_n:
180.                     return 'next'
181.                 elif event.key == K_b:
182.                     return 'back'
183.
184.                 elif event.key == K_ESCAPE:
185.                     terminate() # Esc key quits.
186.                 elif event.key == K_BACKSPACE:
187.                     return 'reset' # Reset the level.
188.                 elif event.key == K_p:
189.                     # Change the player image to the next one.
190.                     currentImage += 1
191.                     if currentImage >= len(PLAYERIMAGES):
192.                         # After the last player image, use the first one.
193.                         currentImage = 0
194.                     mapNeedsRedraw = True
195.
196.             elif event.type == KEYUP:
197.                 # Unset the camera move mode.
198.                 if event.key == K_a:
199.                     cameraLeft = False
200.                 elif event.key == K_d:
201.                     cameraRight = False
202.                 elif event.key == K_w:
203.                     cameraUp = False
204.                 elif event.key == K_s:
205.                     cameraDown = False

This code handles what to do when the various keys are pressed.

207.         if playerMoveTo != None and not levelIsComplete:
208.             # If the player pushed a key to move, make the move
209.             # (if possible) and push any stars that are pushable.
210.             moved = makeMove(mapObj, gameStateObj, playerMoveTo)
211.
212.             if moved:
213.                 # increment the step counter.
214.                 gameStateObj['stepCounter'] += 1
215.                 mapNeedsRedraw = True
216.
217.             if isLevelFinished(levelObj, gameStateObj):
218.                 # level is solved, we should show the "Solved!" image.
219.                 levelIsComplete = True
220.                 keyPressed = False

If the playerMoveTo variable is no longer set to None, then we know the player intended to move. The call to makeMove() handles changing the XY coordinates of the player’s position in the gameStateObj, as well as pushing any stars. The return value of makeMove() is stored in moved. If this value is True, then the player character was moved in that direction. If the value was False, then the player must have tried to move into a tile that was a wall, or push a star that had something behind it. In this case, the player can’t move and nothing on the map changes.

222.         DISPLAYSURF.fill(BGCOLOR)
223.
224.         if mapNeedsRedraw:
225.             mapSurf = drawMap(mapObj, gameStateObj, levelObj['goals'])
226.             mapNeedsRedraw = False

The map does not need to be redrawn on each iteration through the game loop. In fact, this game program is complicated enough that doing so would cause a slight (but noticeable) slowdown in the game. And the map really only needs to be redrawn when something has changed (such as the player moving or a star being pushed). So the Surface object in the mapSurf variable is only updated with a call to the drawMap() function when the mapNeedsRedraw variable is set to True.

After the map has been drawn on line 225, the mapNeedsRedraw variable is set to False. If you want to see how the program slows down by drawing on each iteration through the game loop, comment out line 226 and rerun the program. You will notice that moving the camera is significantly slower.

228.         if cameraUp and cameraOffsetY < MAX_CAM_X_PAN: 
229.             cameraOffsetY += CAM_MOVE_SPEED 
230.         elif cameraDown and cameraOffsetY > -MAX_CAM_X_PAN:
231.             cameraOffsetY -= CAM_MOVE_SPEED
232.         if cameraLeft and cameraOffsetX < MAX_CAM_Y_PAN: 
233.             cameraOffsetX += CAM_MOVE_SPEED 
234.         elif cameraRight and cameraOffsetX > -MAX_CAM_Y_PAN: 
235.             cameraOffsetX -= CAM_MOVE_SPEED 

If the camera movement variables are set to True and the camera has not gone past (i.e. panned passed) the boundaries set by the MAX_CAM_X_PAN and MAX_CAM_Y_PAN, then the camera location (stored in cameraOffsetX and cameraOffsetY) should move over by CAM_MOVE_SPEED pixels.

Note that there is an if and elif statement on lines 228 and 230 for moving the camera up and down, and then a separate if and elif statement on lines 232 and 234. This way, the user can move the camera both vertically and horizontally at the same time. This wouldn’t be possible if line 232 were an elif statement.

237.         # Adjust mapSurf's Rect object based on the camera offset.
238.         mapSurfRect = mapSurf.get_rect()
239.         mapSurfRect.center = (HALF_WINWIDTH + cameraOffsetX,
HALF_WINHEIGHT + cameraOffsetY)
240.
241.         # Draw mapSurf to the DISPLAYSURF Surface object.
242.         DISPLAYSURF.blit(mapSurf, mapSurfRect)
243.
244.         DISPLAYSURF.blit(levelSurf, levelRect)
245.         stepSurf = BASICFONT.render('Steps: %s' %
(gameStateObj['stepCounter']), 1, TEXTCOLOR)
246.         stepRect = stepSurf.get_rect()
247.         stepRect.bottomleft = (20, WINHEIGHT - 10)
248.         DISPLAYSURF.blit(stepSurf, stepRect)
249.
250.         if levelIsComplete:
251.             # is solved, show the "Solved!" image until the player
252.             # has pressed a key.
253.             solvedRect = IMAGESDICT['solved'].get_rect()
254.             solvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT)
255.             DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect)
256.
257.             if keyPressed:
258.                 return 'solved'
259.
260.         pygame.display.update() # draw DISPLAYSURF to the screen.
261.         FPSCLOCK.tick()

Lines 237 to 261 position the camera and draw the map and other graphics to the display Surface object in DISPLAYSURF. If the level is solved, then the victory graphic is also drawn on top of everything else. The keyPressed variable will be set to True if the user pressed a key during this iteration, at which point the runLevel() function returns.

264. def isWall(mapObj, x, y):
265.     """Returns True if the (x, y) position on
266.     the map is a wall, otherwise return False."""
267.     if x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]): 
268.         return False # x and y aren't actually on the map. 
269.     elif mapObj[x][y] in ('#', 'x'): 
270.         return True # wall is blocking 
271.     return False 

The isWall() function returns True if there is a wall on the map object at the XY coordinates passed to the function. Wall objects are represented as either a 'x' or '#' string in the map object.

274. def decorateMap(mapObj, startxy):
275.     """Makes a copy of the given map object and modifies it.
276.     Here is what is done to it:
277.         * Walls that are corners are turned into corner pieces.
278.         * The outside/inside floor tile distinction is made.
279.         * Tree/rock decorations are randomly added to the outside tiles.
280.
281.     Returns the decorated map object."""
282.
283.     startx, starty = startxy # Syntactic sugar
284.
285.     # Copy the map object so we don't modify the original passed
286.     mapObjCopy = copy.deepcopy(mapObj)

The decorateMap() function alters the data structure mapObj so that it isn’t as plain as it appears in the map file. The three things that decorateMap() changes are explained in the comment at the top of the function.

288.     # Remove the non-wall characters from the map data
289.     for x in range(len(mapObjCopy)):
290.         for y in range(len(mapObjCopy[0])):
291.             if mapObjCopy[x][y] in ('$', '.', '@', '+', '*'):
292.                 mapObjCopy[x][y] = ' '

The map object has characters that represent the position of the player, goals, and stars. These are necessary for the map object (they’re stored in other data structures after the map file is read) so they are converted to blank spaces.

294.     # Flood fill to determine inside/outside floor tiles.
295.     floodFill(mapObjCopy, startx, starty, ' ', 'o')

The floodFill() function will change all of the tiles inside the walls from ' ' characters to 'o' characters. It does this using a programming concept called recursion, which is explained in "Recursive Functions" section later in this chapter.

297.     # Convert the adjoined walls into corner tiles.
298.     for x in range(len(mapObjCopy)):
299.         for y in range(len(mapObjCopy[0])):
300.
301.             if mapObjCopy[x][y] == '#':
302.                 if (isWall(mapObjCopy, x, y-1) and isWall(mapObjCopy, x+1,
y)) or \
303.                    (isWall(mapObjCopy, x+1, y) and isWall(mapObjCopy, x,
y+1)) or \
304.                    (isWall(mapObjCopy, x, y+1) and isWall(mapObjCopy, x-1,
y)) or \
305.                    (isWall(mapObjCopy, x-1, y) and isWall(mapObjCopy, x,
y-1)):
306.                     mapObjCopy[x][y] = 'x'
307.
308.             elif mapObjCopy[x][y] == ' ' and random.randint(0, 99) < 
OUTSIDE_DECORATION_PCT: 
309.                 mapObjCopy[x][y] = 
random.choice(list(OUTSIDEDECOMAPPING.keys())) 
310.  
311.     return mapObjCopy  

The large, multi-line if statement on line 301 checks if the wall tile at the current XY coordinates are a corner wall tile by checking if there are wall tiles adjacent to it that form a corner shape. If so, the '#' string in the map object that represents a normal wall is changed to a 'x' string which represents a corner wall tile.

314. def isBlocked(mapObj, gameStateObj, x, y):
315.     """Returns True if the (x, y) position on the map is
316.     blocked by a wall or star, otherwise return False."""
317.
318.     if isWall(mapObj, x, y):
319.         return True
320.
321.     elif x < 0 or x >= len(mapObj) or y < 0 or y >= len(mapObj[x]): 
322.         return True # x and y aren't actually on the map. 
323. 
324.     elif (x, y) in gameStateObj['stars']: 
325.         return True # a star is blocking 
326.  
327.     return False 

There are three cases where a space on the map would be blocked: if there is a star, a wall, or the coordinates of the space are past the edges of the map. The isBlocked() function checks for these three cases and returns True if the XY coordinates are blocked and False if not.

330. def makeMove(mapObj, gameStateObj, playerMoveTo):
331.     """Given a map and game state object, see if it is possible for the
332.     player to make the given move. If it is, then change the player's
333.     position (and the position of any pushed star). If not, do nothing.
334.
335.     Returns True if the player moved, otherwise False."""
336.
337.     # Make sure the player can move in the direction they want.
338.     playerx, playery = gameStateObj['player']
339.
340.     # This variable is "syntactic sugar". Typing "stars" is more
341.     # readable than typing "gameStateObj['stars']" in our code.
342.     stars = gameStateObj['stars']
343.
344.     # The code for handling each of the directions is so similar aside
345.     # from adding or subtracting 1 to the x/y coordinates. We can
346.     # simplify it by using the xOffset and yOffset variables.
347.     if playerMoveTo == UP:
348.         xOffset = 0
349.         yOffset = -1
350.     elif playerMoveTo == RIGHT:
351.         xOffset = 1
352.         yOffset = 0
353.     elif playerMoveTo == DOWN:
354.         xOffset = 0
355.         yOffset = 1
356.     elif playerMoveTo == LEFT:
357.         xOffset = -1
358.         yOffset = 0
359.
360.     # See if the player can move in that direction.
361.     if isWall(mapObj, playerx + xOffset, playery + yOffset):
362.         return False
363.     else:
364.         if (playerx + xOffset, playery + yOffset) in stars:
365.             # There is a star in the way, see if the player can push it.
366.             if not isBlocked(mapObj, gameStateObj, playerx + (xOffset*2),
playery + (yOffset*2)):
367.                 # Move the star.
368.                 ind = stars.index((playerx + xOffset, playery + yOffset))
369.                 stars[ind] = (stars[ind][0] + xOffset, stars[ind][1] +
yOffset)
370.             else:
371.                 return False
372.         # Move the player upwards.
373.         gameStateObj['player'] = (playerx + xOffset, playery + yOffset)
374.         return True

The makeMove() function checks to make sure if moving the player in a particular direction is a valid move. As long as there isn’t a wall blocking the path, or a star that has a wall or star behind it, the player will be able to move in that direction. The gameStateObj variable will be updated to reflect this, and the True value will be returned to tell the function’s caller that the player was moved.

If there was a star in the space that the player wanted to move, that star’s position is also changed and this information is updated in the gameStateObj variable as well. This is how the "star pushing" is implemented.

If the player is blocked from moving in the desired direction, then the gameStateObj is not modified and the function returns False.

377. def startScreen():
378.     """Display the start screen (which has the title and instructions)
379.     until the player presses a key. Returns None."""
380.
381.     # Position the title image.
382.     titleRect = IMAGESDICT['title'].get_rect()
383.     topCoord = 50 # topCoord tracks where to position the top of the text
384.     titleRect.top = topCoord
385.     titleRect.centerx = HALF_WINWIDTH
386.     topCoord += titleRect.height
387.
388.     # Unfortunately, Pygame's font & text system only shows one line at
389.     # a time, so we can't use strings with \n newline characters in them.
390.     # So we will use a list with each line in it.
391.     instructionText = ['Push the stars over the marks.',
392.                        'Arrow keys to move, WASD for camera control, P to
change character.',
393.                        'Backspace to reset level, Esc to quit.',
394.                        'N for next level, B to go back a level.']

The startScreen() function needs to display a few different pieces of text down the center of the window. We will store each line as a string in the instructionText list. The title image (stored in IMAGESDICT['title'] as a Surface object (that was originally loaded from the star_title.png file)) will be positioned 50 pixels from the top of the window. This is because the integer 50 was stored in the topCoord variable on line 383. The topCoord variable will track the Y axis positioning of the title image and the instructional text. The X axis is always going to be set so that the images and text are centered, as it is on line 385 for the title image.

On line 386, the topCoord variable is increased by whatever the height of that image is. This way we can modify the image and the start screen code won’t have to be changed.

396.     # Start with drawing a blank color to the entire window:
397.     DISPLAYSURF.fill(BGCOLOR)
398.
399.     # Draw the title image to the window:
400.     DISPLAYSURF.blit(IMAGESDICT['title'], titleRect)
401.
402.     # Position and draw the text.
403.     for i in range(len(instructionText)):
404.         instSurf = BASICFONT.render(instructionText[i], 1, TEXTCOLOR)
405.         instRect = instSurf.get_rect()
406.         topCoord += 10 # 10 pixels will go in between each line of text.
407.         instRect.top = topCoord
408.         instRect.centerx = HALF_WINWIDTH
409.         topCoord += instRect.height # Adjust for the height of the line.
410.         DISPLAYSURF.blit(instSurf, instRect)

Line 400 is where the title image is blitted to the display Surface object. The for loop starting on line 403 will render, position, and blit each instructional string in the instructionText loop. The topCoord variable will always be incremented by the size of the previously rendered text (line 409) and 10 additional pixels (on line 406, so that there will be a 10 pixel gap between the lines of text).

412.     while True: # Main loop for the start screen.
413.         for event in pygame.event.get():
414.             if event.type == QUIT:
415.                 terminate()
416.             elif event.type == KEYDOWN:
417.                 if event.key == K_ESCAPE:
418.                     terminate()
419.                 return # user has pressed a key, so return.
420.
421.         # Display the DISPLAYSURF contents to the actual screen.
422.         pygame.display.update()
423.         FPSCLOCK.tick()

There is a game loop in startScreen() that begins on line 412 and handles events that indicate if the program should terminate or return from the startScreen() function. Until the player does either, the loop will keep calling pygame.display.update() and FPSCLOCK.tick() to keep the start screen displayed on the screen.

< Лекция 8 || Лекция 9: 123 || Лекция 10 >