Опубликован: 06.08.2013 | Уровень: для всех | Доступ: платный
Лекция 9:

Star Pusher

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

Data Structures in Star Pusher

Star Pusher has a specific format for the levels, maps, and game state data structures.

The "Game State" Data Structure

The game state object will be a dictionary with three keys: 'player', 'stepCounter', and 'stars'.

  • The value at the 'player' key will be a tuple of two integers for the current XY position of the player.
  • The value at the 'stepCounter' key will be an integer that tracks how many moves the player has made in this level (so the player can try to solve the puzzle in the future with fewer steps).
  • The value at the 'stars' key is a list of two-integer tuples of XY values for each of the stars on the current level.

The "Map" Data Structure

The map data structure is simply a 2D list of lists where the two indexes used represent the X and Y coordinates of the map. The value at each index in the list of lists is a single-character string that represents the title that is on that map at each space:

  • '#' – A wooden wall.
  • 'x' – A corner wall.
  • '@' – The starting space for the player on this level.
  • '.' – A goal space.
  • '$' – A space where a star is at the start of the level.
  • '+' – A space with a goal and the starting player’s space.
  • '*' – A space with a goal and a star at the start of the level.
  • ' ' – A grassy outdoor space.
  • 'o' – An inside floor space (This is a lowercase letter O, not a zero).
  • '1' – A rock on grass.
  • '2' – A short tree on grass.
  • '3' – A tall tree on grass.
  • '4' – An ugly tree on grass.

The "Levels" Data Structure

The level object contains a game state object (which will be the state used when the level first starts), a map object, and a few other values. The level object itself is a dictionary with the following keys:

  • The value at the key 'width' is an integer of how many tiles wide the entire map is.
  • The value at the key 'height' is an integer of how many tiles tall the entire map is.
  • The value at the key 'mapObj' is the map object for this level.
  • The value at the key 'goals' is a list of two-integer tuples with the XY coordinates of each goal space on the map.
  • The value at the key 'startState' is a game state object used to show the starting position of the stars and player at the start of the level.

Reading and Writing Text Files

Python has functions for reading files off of the player’s hard drive. This will be useful for having a separate file keep all of the data for each level. This is also a good idea because in order to get new levels, the player doesn’t have to change the source code of the game but instead can just download new level files.

Text Files and Binary Files

Text files are files that contain simple text data. Text files are created in Windows by the Notepad application, Gedit on Ubuntu, and TextEdit on Mac OS X. There are many other programs called text editors that can create and modify text files. IDLE’s own file editor is a text editor.

The difference between text editors and word processors (like Microsoft Word, or OpenOffice Writer, or iWork Pages) is that text editors have text only. You can’t set the font, size, or color of the text. (IDLE automatically sets the color of the text based on what kind of Python code it is, but you can’t change this yourself, so it is still a text editor.) The difference between text and binary files isn’t important for this game program, but you can read about it at http://invpy.com/textbinary . All you need to know is the this chapter and the Star Pusher program only deal with text files.

Writing to Files

To create a file, call the open() function pass it two arguments: a string for the name of the file, and the string 'w' to tell the open() function you want to open the file in "write" mode. The open() function returns a file object:

>>> textFile = open('hello.txt', 'w')
>>>

If you run this code from the interactive shell, the hello.txt file that this function creates will be created in the same folder that the python.exe program is in (on Windows, this will probably be C:\Python32). If the open() function is called from a .py program, the file is created in the same folder that the .py file is in.

The "write" mode tells open() to create the file if it does not exist. If it does exist, then open() will delete that file and create a new, blank file. This is just like how an assignment statement can create a new variable, or overwrite the current value in an already existing variable. This can be somewhat dangerous. If you accidentally send a filename of an important file to the open() function with 'w' as the second parameter, it will be deleted. This could result in having to reinstall the operating system on your computer and/or the launching of nuclear missiles.

The file object has a method called write() which can be used to write text to the file. Just pass it a string like you would pass a string to the print() function. The difference is that write() does not automatically add a newline character ('\n') to the end of the string. If you want to add a newline, you will have to include it in the string:

>>> textFile = open('hello.txt', 'w')
>>> textFile.write('This will be the content of the file.\nHello world!\n')
>>>

To tell Python that you are done writing content to this file, you should call the close() method of the file object (Although Python will automatically close any opened file objects when the program ends).

>>> textFile.close()

Reading from Files

To read the content of a file, pass the string 'r' instead of 'w' to the open() function. Then call the readlines() method on the file object to read in the contents of the file. Last, close the file by calling the close() method.

>>> textFile = open('hello.txt', 'r')
>>> content = textFile.readlines()
>>> textFile.close()

The readlines() method returns a list of strings: one string for each line of text in the file:

>>> content
['This will be the content of the file.\n', 'Hello world!\n']
>>>

If you want to re-read the contents of that file, you will have to call close() on the file object and re-open it.

As an alternative to readlines(), you can also call the read() method, which will return the entire contents of the file as a single string value:

>>> textFile = open('hello.txt', 'r')
>>> content = textFile.read()
>>> content
'This will be the content of the file.\nHello world!\n'

On a side note, if you leave out the second parameter to the open() function, Python will assume you mean to open the file in read mode. So open('foobar.txt', 'r') and open('foobar.txt') do the exact same thing.

About the Star Pusher Map File Format

We need the level text file to be in a specific format. Which characters represent walls, or stars, or the player’s starting position? If we have the maps for multiple levels, how can we tell when one level’s map ends and the next one begins?

Fortunately, the map file format we will use is already defined for us. There are many Sokoban games out there (you can find more at http://invpy.com/sokobanclones), and they all use the same map file format. If you download the levels file from http://invpy.com/starPusherLevels.txt and open it in a text editor, you’ll see something like this:

; Star Pusher (Sokoban clone)
; http://inventwithpython.com/blog
; By Al Sweigart al@inventwithpython.com
;
; Everything after the ; is a comment and will be ignored by the game that
; reads in this file.
;
; The format is described at:
; http://sokobano.de/wiki/index.php?title=Level_format
;   @ - The starting position of the player.
;   $ - The starting position for a pushable star.
;   . - A goal where a star needs to be pushed.
;   + - Player & goal
;   * - Star & goal
;  (space) - an empty open space.
;   # - A wall.
;
; Level maps are separated by a blank line (I like to use a ; at the start
; of the line since it is more visible.)
;
; I tried to use the same format as other people use for their Sokoban games,
; so that loading new levels is easy. Just place the levels in a text file
; and name it "starPusherLevels.txt" (after renaming this file, of course).
; Starting demo level:
########
##      #
#   .   #
#   $   #
# .$@$. #
####$   #
#.   #
#   ##
#####

The comments at the top of the file explain the file’s format. When you load the first level, it looks like this:


Рис. 9.3.
426. def readLevelsFile(filename):
427.     assert os.path.exists(filename), 'Cannot find the level file: %s' %
(filename)

The os.path.exists() function will return True if the file specified by the string passed to the function exists. If it does not exist, os.path.exists() returns False.

428.     mapFile = open(filename, 'r')
429.     # Each level must end with a blank line
430.     content = mapFile.readlines() + ['\r\n']
431.     mapFile.close()
432.
433.     levels = [] # Will contain a list of level objects.
434.     levelNum = 0
435.     mapTextLines = [] # contains the lines for a single level's map.
436.     mapObj = [] # the map object made from the data in mapTextLines

The file object for the level file that is opened for reading is stored in mapFile. All of the text from the level file is stored as a list of strings in the content variable, with a blank line added to the end (The reason that this is done is explained later).

After the level objects are created, they will be stored in the levels list. The levelNum variable will keep track of how many levels are found inside the level file. The mapTextLines list will be a list of strings from the content list for a single map (as opposed to how content stores the strings of all maps in the level file). The mapObj variable will be a 2D list.

437.     for lineNum in range(len(content)):
438.         # Process each line that was in the level file.
439.         line = content[lineNum].rstrip('\r\n')

The for loop on line 437 will go through each line that was read from the level file one line at a time. The line number will be stored in lineNum and the string of text for the line will be stored in line. Any newline characters at the end of the string will be stripped off.

441.         if ';' in line:
442.             # Ignore the ; lines, they're comments in the level file.
443.             line = line[:line.find(';')]

Any text that exists after a semicolon in the map file is treated like a comment and is ignored. This is just like the # sign for Python comments. To make sure that our code does not accidentally think the comment is part of the map, the line variable is modified so that it only consists of the text up to (but not including) the semicolon character (Remember that this is only changing the string in the content list. It is not changing the level file on the hard drive).

445.         if line != '':
446.             # This line is part of the map.
447.             mapTextLines.append(line)

There can be maps for multiple levels in the map file. The mapTextLines list will contain the lines of text from the map file for the current level being loaded. As long as the current line is not blank, the line will be appended to the end of mapTextLines.

448.         elif line == '' and len(mapTextLines) > 0:
449.             # A blank line indicates the end of a level's map in the file.
450.             # Convert the text in mapTextLines into a level object.

When there is a blank line in the map file, that indicates that the map for the current level has ended. And future lines of text will be for the later levels. Note however, that there must at least be one line in mapTextLines so that multiple blank lines together are not counted as the start and stop to multiple levels.

452.             # Find the longest row in the map.
453.             maxWidth = -1
454.             for i in range(len(mapTextLines)):
455.                 if len(mapTextLines[i]) > maxWidth:
456.                     maxWidth = len(mapTextLines[i])

All of the strings in mapTextLines need to be the same length (so that they form a rectangle), so they should be padded with extra blank spaces until they are all as long as the longest string. The for loop goes through each of the strings in mapTextLines and updates maxWidth when it finds a new longest string. After this loop finishes executing, the maxWidth variable will be set to the length of the longest string in mapTextLines.

457.             # Add spaces to the ends of the shorter rows. This
458.             # ensures the map will be rectangular.
459.             for i in range(len(mapTextLines)):
460.                 mapTextLines[i] += ' ' * (maxWidth - len(mapTextLines[i]))

The for loop on line 459 goes through the strings in mapTextLines again, this time to add enough space characters to pad each to be as long as maxWidth.

462.             # Convert mapTextLines to a map object.
463.             for x in range(len(mapTextLines[0])):
464.                 mapObj.append([])
465.             for y in range(len(mapTextLines)):
466.                 for x in range(maxWidth):
467.                     mapObj[x].append(mapTextLines[y][x])

The mapTextLines variable just stores a list of strings. (Each string in the list represents a row, and each character in the string represents a character at a different column. This is why line 467 has the Y and X indexes reversed, just like the SHAPES data structure in the Tetromino game.) But the map object will have to be a list of list of single-character strings such that mapObj[x][y] refers to the tile at the XY coordinates. The for loop on line 463 adds an empty list to mapObj for each column in mapTextLines.

The nested for loops on line 465 and 466 will fill these lists with single-character strings to represent each tile on the map. This creates the map object that Star Pusher uses.

469.             # Loop through the spaces in the map and find the @, ., and $
470.             # characters for the starting game state.
471.             startx = None # The x and y for the player's starting position
472.             starty = None
473.             goals = [] # list of (x, y) tuples for each goal.
474.             stars = [] # list of (x, y) for each star's starting position.
475.             for x in range(maxWidth):
476.                 for y in range(len(mapObj[x])):
477.                     if mapObj[x][y] in ('@', '+'):
478.                         # '@' is player, '+' is player & goal
479.                         startx = x
480.                         starty = y
481.                     if mapObj[x][y] in ('.', '+', '*'):
482.                         # '.' is goal, '*' is star & goal
483.                         goals.append((x, y))
484.                     if mapObj[x][y] in ('$', '*'):
485.                         # '$' is star
486.                         stars.append((x, y))

After creating the map object, the nested for loops on lines 475 and 476 will go through each space to find the XY coordinates three things:

  1. The player’s starting position. This will be stored in the startx and starty variables, which will then be stored in the game state object later on line 494.
  2. The starting position of all the stars These will be stored in the stars list, which is later stored in the game state object on line 496.
  3. The position of all the goals. These will be stored in the goals list, which is later stored in the level object on line 500.

Remember, the game state object contains all the things that can change. This is why the player’s position is stored in it (because the player can move around) and why the stars are stored in it (because the stars can be pushed around by the player). But the goals are stored in the level object, since they will never move around.

488.             # Basic level design sanity checks:
489.             assert startx != None and starty != None, 'Level %s (around
line %s) in %s is missing a "@" or "+" to mark the start point.' % (levelNum+1,
lineNum, filename)
490.             assert len(goals) > 0, 'Level %s (around line %s) in %s must
have at least one goal.' % (levelNum+1, lineNum, filename)
491.             assert len(stars) >= len(goals), 'Level %s (around line %s) in
%s is impossible to solve. It has %s goals but only %s stars.' % (levelNum+1,
lineNum, filename, len(goals), len(stars))

At this point, the level has been read in and processed. To be sure that this level will work properly, a few assertions must pass. If any of the conditions for these assertions are False, then Python will produce an error (using the string from the assert statement) saying what is wrong with the level file.

The first assertion on line 489 checks to make sure that there is a player starting point listed somewhere on the map. The second assertion on line 490 checks to make sure there is at least one goal (or more) somewhere on the map. And the third assertion on line 491 checks to make sure that there is at least one star for each goal (but having more stars than goals is allowed).

493.             # Create level object and starting game state object.
494.             gameStateObj = {'player': (startx, starty),
495.                             'stepCounter': 0,
496.                             'stars': stars}
497.             levelObj = {'width': maxWidth,
498.                         'height': len(mapObj),
499.                         'mapObj': mapObj,
500.                         'goals': goals,
501.                         'startState': gameStateObj}
502.
503.             levels.append(levelObj)

Finally, these objects are stored in the game state object, which itself is stored in the level object. The level object is added to a list of level objects on line 503. It is this levels list that will be returned by the readLevelsFile() function when all of the maps have been processed.

505.             # Reset the variables for reading the next map.
506.             mapTextLines = []
507.             mapObj = []
508.             gameStateObj = {}
509.             levelNum += 1
510.     return levels

Now that this level is done processing, the variables for mapTextLines, mapObj, and gameStateObj should be reset to blank values for the next level that will be read in from the level file. The levelNum variable is also incremented by 1 for the next level’s level number.

< Лекция 8 || Лекция 9: 123 || Лекция 10 >
Алексей Маряскин
Алексей Маряскин
Россия
Алина Лаврик
Алина Лаврик
Украина, Харьков, Харьковский университет воздушных сил, 2008