Code:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import ConfigParser
import os
import pygame
import pygame.locals as pg
from twisted.python import log
# Motion offsets for particular directions
# N E S W
DX = [0, 1, 0, -1]
DY = [-1, 0, 1, 0]
# Dimensions of the map tiles
MAP_TILE_WIDTH, MAP_TILE_HEIGHT = 24, 16
class TileCache:
"""Load the tilesets lazily into global cache"""
def __init__(self, width=32, height=None):
self.width = width
self.height = height or width
self.cache = {}
def __getitem__(self, filename):
"""Return a table of tiles, load it from disk if needed."""
key = (filename, self.width, self.height)
try:
return self.cache[key]
except KeyError:
tile_table = self._load_tile_table(filename, self.width,
self.height)
self.cache[key] = tile_table
return tile_table
def _load_tile_table(self, filename, width, height):
"""Load an image and split it into tiles."""
image = pygame.image.load(filename).convert()
image_width, image_height = image.get_size()
tile_table = []
for tile_x in range(0, image_width/width):
line = []
tile_table.append(line)
for tile_y in range(0, image_height/height):
rect = (tile_x*width, tile_y*height, width, height)
line.append(image.subsurface(rect))
return tile_table
class SortedUpdates(pygame.sprite.RenderUpdates):
"""A sprite group that sorts them by depth."""
def sprites(self):
"""The list of sprites in the group, sorted by depth."""
return sorted(self.spritedict.keys(), key=lambda sprite: sprite.depth)
class Shadow(pygame.sprite.Sprite):
"""Sprite for shadows."""
def __init__(self, owner):
pygame.sprite.Sprite.__init__(self)
self.image = SPRITE_CACHE["shadow.png"][0][0]
self.image.set_alpha(64)
self.rect = self.image.get_rect()
self.owner = owner
def update(self, *args):
"""Make the shadow follow its owner."""
self.rect.midbottom = self.owner.rect.midbottom
class Sprite(pygame.sprite.Sprite):
"""Sprite for animated items and base class for Player."""
is_player = False
def __init__(self, pos=(0, 0), frames=None):
super(Sprite, self).__init__()
if frames:
self.frames = frames
self.image = self.frames[0][0]
self.rect = self.image.get_rect()
self.animation = self.stand_animation()
self.pos = pos
def _get_pos(self):
"""Check the current position of the sprite on the map."""
return (self.rect.midbottom[0]-12)/24, (self.rect.midbottom[1]-16)/16
def _set_pos(self, pos):
"""Set the position and depth of the sprite on the map."""
self.rect.midbottom = pos[0]*24+12, pos[1]*16+16
self.depth = self.rect.midbottom[1]
pos = property(_get_pos, _set_pos)
def move(self, dx, dy):
"""Change the position of the sprite on screen."""
self.rect.move_ip(dx, dy)
self.depth = self.rect.midbottom[1]
def stand_animation(self):
"""The default animation."""
while True:
# Change to next frame every two ticks
for frame in self.frames[0]:
self.image = frame
yield None
yield None
def update(self, *args):
"""Run the current animation."""
self.animation.next()
class Player(Sprite):
""" Display and animate the player character."""
is_player = True
def __init__(self, pos=(1, 1)):
self.frames = SPRITE_CACHE["player.png"]
Sprite.__init__(self, pos)
self.direction = 2
self.animation = None
self.image = self.frames[self.direction][0]
def walk_animation(self):
"""Animation for the player walking."""
# This animation is hardcoded for 4 frames and 16x24 map tiles
for frame in range(4):
self.image = self.frames[self.direction][frame]
yield None
self.move(3*DX[self.direction], 2*DY[self.direction])
yield None
self.move(3*DX[self.direction], 2*DY[self.direction])
def update(self, *args):
"""Run the current animation or just stand there if no animation set."""
if self.animation is None:
self.image = self.frames[self.direction][0]
else:
try:
self.animation.next()
except StopIteration:
self.animation = None
class Level(object):
"""Load and store the map of the level, together with all the items."""
def __init__(self, filename="level.map"):
self.tileset = ''
self.map = []
self.items = {}
self.key = {}
self.width = 0
self.height = 0
self.load_file(filename)
def load_file(self, filename="level.map"):
"""Load the level from specified file."""
parser = ConfigParser.ConfigParser()
parser.read(filename)
self.tileset = parser.get("level", "tileset")
self.map = parser.get("level", "map").split("\n")
for section in parser.sections():
if len(section) == 1:
desc = dict(parser.items(section))
self.key[section] = desc
self.width = len(self.map[0])
self.height = len(self.map)
for y, line in enumerate(self.map):
for x, c in enumerate(line):
if not self.is_wall(x, y) and 'sprite' in self.key[c]:
self.items[(x, y)] = self.key[c]
def render(self):
"""Draw the level on the surface."""
wall = self.is_wall
tiles = MAP_CACHE[self.tileset]
image = pygame.Surface((self.width*MAP_TILE_WIDTH, self.height*MAP_TILE_HEIGHT))
overlays = {}
for map_y, line in enumerate(self.map):
for map_x, c in enumerate(line):
if wall(map_x, map_y):
# Draw different tiles depending on neighbourhood
if not wall(map_x, map_y+1):
if wall(map_x+1, map_y) and wall(map_x-1, map_y):
tile = 1, 2
elif wall(map_x+1, map_y):
tile = 0, 2
elif wall(map_x-1, map_y):
tile = 2, 2
else:
tile = 3, 2
else:
if wall(map_x+1, map_y+1) and wall(map_x-1, map_y+1):
tile = 1, 1
elif wall(map_x+1, map_y+1):
tile = 0, 1
elif wall(map_x-1, map_y+1):
tile = 2, 1
else:
tile = 3, 1
# Add overlays if the wall may be obscuring something
if not wall(map_x, map_y-1):
if wall(map_x+1, map_y) and wall(map_x-1, map_y):
over = 1, 0
elif wall(map_x+1, map_y):
over = 0, 0
elif wall(map_x-1, map_y):
over = 2, 0
else:
over = 3, 0
overlays[(map_x, map_y)] = tiles[over[0]][over[1]]
else:
try:
tile = self.key[c]['tile'].split(',')
tile = int(tile[0]), int(tile[1])
except (ValueError, KeyError):
# Default to ground tile
tile = 0, 3
tile_image = tiles[tile[0]][tile[1]]
image.blit(tile_image,
(map_x*MAP_TILE_WIDTH, map_y*MAP_TILE_HEIGHT))
return image, overlays
def get_tile(self, x, y):
"""Tell what's at the specified position of the map."""
try:
char = self.map[y][x]
except IndexError:
return {}
try:
return self.key[char]
except KeyError:
return {}
def get_bool(self, x, y, name):
"""Tell if the specified flag is set for position on the map."""
value = self.get_tile(x, y).get(name)
return value in (True, 1, 'true', 'yes', 'True', 'Yes', '1', 'on', 'On')
def is_wall(self, x, y):
"""Is there a wall?"""
return self.get_bool(x, y, 'wall')
def is_blocking(self, x, y):
"""Is this place blocking movement?"""
if not 0 <= x < self.width or not 0 <= y < self.height:
return True
return self.get_bool(x, y, 'block')
class Game:
"""The main game object."""
def __init__(self):
self.screen = pygame.display.get_surface()
self.pressed_key = None
self.game_over = False
self.shadows = pygame.sprite.RenderUpdates()
self.sprites = SortedUpdates()
self.overlays = pygame.sprite.RenderUpdates()
self.use_level(Level())
def use_level(self, level):
"""Set the level as the current one."""
self.shadows = pygame.sprite.RenderUpdates()
self.sprites = SortedUpdates()
self.overlays = pygame.sprite.RenderUpdates()
self.level = level
# Populate the game with the level's objects
for pos, tile in level.items.iteritems():
if tile.get("player") in ('true', '1', 'yes', 'on'):
sprite = Player(pos)
self.player = sprite
else:
sprite = Sprite(pos, TileCache()[tile["sprite"]])
self.sprites.add(sprite)
self.shadows.add(Shadow(sprite))
# Render the level map
self.background, overlays = self.level.render()
# Add the overlays for the level map
for (x, y), image in overlays.iteritems():
overlay = pygame.sprite.Sprite(self.overlays)
overlay.image = image
overlay.rect = image.get_rect().move(x*24, y*16-16)
def control(self):
"""Handle the controls of the game."""
keys = pygame.key.get_pressed()
def pressed(key):
"""Check if the specified key is pressed."""
return self.pressed_key == key or keys[key]
def walk(d):
"""Start walking in specified direction."""
x, y = self.player.pos
self.player.direction = d
if not self.level.is_blocking(x+DX[d], y+DY[d]):
self.player.animation = self.player.walk_animation()
if pressed(pg.K_UP):
walk(0)
elif pressed(pg.K_DOWN):
walk(2)
elif pressed(pg.K_LEFT):
walk(3)
elif pressed(pg.K_RIGHT):
walk(1)
self.pressed_key = None
def main(self):
"""Run the main loop."""
clock = pygame.time.Clock()
# Draw the whole screen initially
self.screen.blit(self.background, (0, 0))
self.overlays.draw(self.screen)
pygame.display.flip()
# The main game loop
while not self.game_over:
# Don't clear shadows and overlays, only sprites.
self.sprites.clear(self.screen, self.background)
self.sprites.update()
# If the player's animation is finished, check for keypresses
if self.player.animation is None:
self.control()
self.player.update()
self.shadows.update()
# Don't add shadows to dirty rectangles, as they already fit inside
# sprite rectangles.
self.shadows.draw(self.screen)
dirty = self.sprites.draw(self.screen)
# Don't add ovelays to dirty rectangles, only the places where
# sprites are need to be updated, and those are already dirty.
self.overlays.draw(self.screen)
# Update the dirty areas of the screen
pygame.display.update(dirty)
# Wait for one tick of the game clock
clock.tick(15)
# Process pygame events
for event in pygame.event.get():
if event.type == pg.QUIT:
self.game_over = True
elif event.type == pg.KEYDOWN:
self.pressed_key = event.key
if __name__ == "__main__":
logfile = open('crash.log','w')
log.startLogging(logfile)
SPRITE_CACHE = TileCache()
MAP_CACHE = TileCache(MAP_TILE_WIDTH, MAP_TILE_HEIGHT)
pygame.init()
pygame.display.set_mode((424, 320))
Game().main()
logfile.flush()
logfile.close()
os.remove('crash.log')
See