Mercurial > mamba
view mamba/level.py @ 561:9afaa1969d6f
Level format 2 support
* * *
Allow for blank author
* * *
Fix thinko in write code
author | Neil Muller <drnlmuller@gmail.com> |
---|---|
date | Tue, 18 Oct 2011 11:41:28 +0200 |
parents | 6c61d5862310 |
children | 2d3dee657638 |
line wrap: on
line source
""" Level for our shiny game. """ import pygame from pygame.surface import Surface from pygame.sprite import RenderUpdates from mamba.constants import UP, DOWN, LEFT, RIGHT, VERSION from mamba.data import load_file from mamba import sprites from StringIO import StringIO class InvalidMapError(Exception): pass def mktile(cls, **kw): return (cls, kw) TILE_MAP = { '.': None, 'X': mktile(sprites.TileSprite, image_name='wall', name='wall', solid=True), 'A': mktile(sprites.TileSprite, image_name='biohazard', name='biohazard', solid=True), 'R': mktile(sprites.DoorSprite, colour='red'), 'B': mktile(sprites.DoorSprite, colour='blue'), 'Y': mktile(sprites.DoorSprite, colour='yellow'), 'e': mktile(sprites.EntrySprite), 'E': mktile(sprites.ExitSprite), '~': mktile(sprites.PuddleSprite), '$': mktile(sprites.FireSprite), 'r': mktile(sprites.Painter, colour='red', name='paint'), 'b': mktile(sprites.Painter, colour='blue', name='paint'), 'y': mktile(sprites.Painter, colour='yellow', name='paint'), '^': mktile(sprites.ArrowSprite, direction=UP), 'v': mktile(sprites.ArrowSprite, direction=DOWN), '<': mktile(sprites.ArrowSprite, direction=LEFT), '>': mktile(sprites.ArrowSprite, direction=RIGHT), '@': mktile(sprites.FlipArrows), } THING_MAP = { 'M': mktile(sprites.BigMouse), 'm': mktile(sprites.SmallMouse), 'f': mktile(sprites.Frog), '&': mktile(sprites.Snail), 'l': mktile(sprites.Lizard), 's': mktile(sprites.Salamander), } class Tileset(object): def __init__(self, tileset_name): self.name = tileset_name self.load_tiles() def load_tiles(self): self.tiles = {} self.floor = sprites.TileSprite('.', tileset=self.name, image_name='floor').image for name, value in TILE_MAP.items(): if value is not None: value[1]['tileset'] = self.name self.tiles[name] = value self.tiles.update(THING_MAP) def __getitem__(self, key): try: tilespec = self.tiles[key] except KeyError: raise InvalidMapError("Unknown tile type: '%s'" % key) if not tilespec: return None cls, params = tilespec params['tile_char'] = key return cls(**params) def get_tile(self, key, tile_pos, *groups): tile = self[key] if tile: tile.add(*groups) tile.set_tile_pos(tile_pos) return tile class Level(object): def __init__(self, level_name, level_namespace, source=None): self.level_name = level_name self.level_namespace = level_namespace self.source = source self.author = '' self.load_level_data() def load_format_1(self, level_data): """Load the original map format""" self.name = level_data[0].strip() tileset_name = level_data[1].strip() self.tileset = Tileset(tileset_name) self.background_track = level_data[2].strip() tiles_ascii = [line.strip() for line in level_data[3:]] try: end = tiles_ascii.index("end") except ValueError: raise InvalidMapError('Missing "end" marker in level') sprites_ascii = tiles_ascii[end + 1:] tiles_ascii = tiles_ascii[:end] self.tiles_ascii = tiles_ascii self.sprites_ascii = sprites_ascii def load_format_2(self, level_data): """Load the second mamba map format""" try: self.name = level_data[1].split('Name:', 1)[1].strip() tileset_name = level_data[3].split('Tileset:', 1)[1].strip() self.background_track = level_data[4].split('Music:', 1)[1].strip() except IndexError: raise InvalidMapError('Incorrectly formatted map header') try: # Missing author isn't fatal self.author = level_data[2].split('Author:', 1)[1].strip() except IndexError: self.author = '' self.tileset = Tileset(tileset_name) tiles_ascii = [line.strip() for line in level_data[5:]] try: end = tiles_ascii.index("end") except ValueError: raise InvalidMapError('Missing "end" marker in level') sprites_ascii = tiles_ascii[end + 1:] tiles_ascii = tiles_ascii[:end] self.tiles_ascii = tiles_ascii self.sprites_ascii = sprites_ascii def check_format(self, level_data): """Determine which format to load""" if (level_data[0].startswith('Version: ') and level_data[3].startswith('Tileset: ')): # Looks like a version 2 level file file_version = level_data[0].split('Version: ', 1)[1].strip() try: file_version = tuple([int(x) for x in file_version.split('.')]) # We only compare on the first two elements, so point # rleases work as expected if file_version[:2] <= VERSION: return 2 else: raise InvalidMapError("Unsupported map version") except ValueError: raise InvalidMapError("Unrecognised map version") else: # We assume anything we don't recognise is format 0 and rely on the # error checking there to save us if we're wrong. return 1 def load_level_data(self): """ This file format is potentially yucky. """ if self.source is not None: level_data = StringIO(self.source) else: level_data = load_file('levels/%s.txt' % (self.level_name,)) # XXX: Should we have some size restriction here? level_data = level_data.readlines() level_format = self.check_format(level_data) if level_format == 2: self.load_format_2(level_data) elif level_format == 1: self.load_format_1(level_data) else: # generic fallback raise InvalidMapError("Unrecognised map version") self.setup_level(self.tiles_ascii, self.sprites_ascii) self.make_background() def validate_level(self): old_tiles_ascii = self.tiles_ascii[:] old_tiles = self.tiles[:] try: self.update_tiles_ascii() self.setup_tiles(self.tiles_ascii) finally: self.tiles = old_tiles self.tiles_ascii = old_tiles_ascii def save_level(self, level_dir=None, is_user_dir=False, save_file=None): """Save the current state of the level""" if save_file is None: if level_dir is None: level_dir = 'levels' file_path = '%s/%s.txt' % (level_dir, self.level_name) save_file = load_file(file_path, 'wb', is_user_dir=is_user_dir) save_file.write('Version: %d.%d\n' % VERSION[:2]) save_file.write('Name: %s\n' % self.name) save_file.write('Author: %s\n' % self.author) save_file.write('Tileset: %s\n' % self.tileset.name) save_file.write('Music: %s\n' % self.background_track) self.update_tiles_ascii() for tile_row in self.tiles_ascii: save_file.write('%s\n' % tile_row) save_file.write('end\n') for sprite_ascii in self.sprites_ascii: save_file.write('%s\n' % sprite_ascii) def unique_name(self): return '/'.join((self.level_namespace, self.level_name)) def update_tiles_ascii(self): """Resync tiles and tile_ascii""" for i, tile_row in enumerate(self.tiles): new_row = [] for tile in tile_row: if tile: new_row.append(tile.tile_char) else: new_row.append('.') self.tiles_ascii[i] = ''.join(new_row) def setup_level(self, tiles_ascii, sprites_ascii): self.sprites = RenderUpdates() self.setup_tiles(tiles_ascii) self.setup_sprites(sprites_ascii) def setup_tiles(self, tiles_ascii): self.tiles = [] self.entry = None self.tile_size = (len(tiles_ascii[0]), len(tiles_ascii)) for y, row in enumerate(tiles_ascii): if len(row) != self.tile_size[0]: raise InvalidMapError("Map not rectangular.") tile_row = [] for x, tile_char in enumerate(row): #tile_orientation = self.get_tile_orientation(y, x, row, # tile_char) tile = self.tileset.get_tile(tile_char, (x, y), self.sprites) tile_row.append(tile) if isinstance(tile, sprites.EntrySprite): self.rejigger_entry_tile(tile) self.tiles.append(tile_row) if self.entry is None: raise InvalidMapError("Not enough entry points.") self.set_tile_orientations() def rejigger_entry_tile(self, tile): if self.entry is not None: raise InvalidMapError("Too many entry points.") self.entry = tile direction = [] x, y = tile.tile_pos if x == 0: direction.append(RIGHT) elif x == self.tile_size[0] - 1: direction.append(LEFT) if y == 0: direction.append(DOWN) elif y == self.tile_size[1] - 1: direction.append(UP) if len(direction) < 1: raise InvalidMapError("Entry point must be along an edge.") if len(direction) > 1: raise InvalidMapError("Entry point must not be on a corner.") tile.set_direction(direction[0]) def set_tile_orientations(self): tiles = [tile # This is a scary listcomp. It makes me happy. for row in self.tiles for tile in row if tile is not None] for tile in tiles: orientation = self.get_tile_orientation(tile) tile.use_variant(*orientation) def setup_sprites(self, sprites_ascii): self.extra_sprites = {} sprite_positions = [] for sprite_ascii in sprites_ascii: try: pos, _sep, rest = sprite_ascii.partition(':') except ValueError: raise InvalidMapError('Unable to determine sprite position' ' from line: %s' % sprite_ascii) try: pos = [int(x.strip()) for x in pos.split(',')] except ValueError: raise InvalidMapError("Sprite position must be two integers." "Got %s" % pos) class_name, rest = rest.split(None, 1) args = rest.split() sprite_id, args = args[0], args[1:] try: cls = sprites.find_sprite(class_name) except KeyError: raise InvalidMapError("Unknown Sprite class: %s" % class_name) sprite = cls(*args) if pos in sprite_positions: raise InvalidMapError('Multiple sprites at %s.' % pos) sprite_positions.append(pos) sprite.set_tile_pos(pos) if sprite_id in self.extra_sprites: raise InvalidMapError('Duplicate sprite id: %s.' % sprite_id) self.extra_sprites[sprite_id] = sprite self.sprites.add(sprite) def is_same_tile(self, tile, x, y): """Is there a tile of the same type?""" if tile.tile_char is None: # This isn't really a tile, so bail return False try: other_tile = self.tiles[y][x] except IndexError: # We're over the edge of the map return False if other_tile is None: # Emptiness. return False return tile.tile_char == other_tile.tile_char def get_tile_orientation(self, tile): if tile is None: return (False, False, False, False) map_x, map_y = tile.tile_pos return ( self.is_same_tile(tile, map_x, map_y - 1), # above self.is_same_tile(tile, map_x, map_y + 1), # below self.is_same_tile(tile, map_x - 1, map_y), # left self.is_same_tile(tile, map_x + 1, map_y), # right ) def get_tile_size(self): return self.tile_size def get_size(self): x, y = self.get_tile_size() return sprites.tile_sizify((x, y)) def make_background(self): if not pygame.display.get_init(): # Skip if we're not actuallt in pygame return sx, sy = self.get_tile_size() self.background = Surface(self.get_size()) [self.background.blit(self.tileset.floor, sprites.tile_sizify((x, y))) for x in range(sx) for y in range(sy)] def get_entry(self): return (self.entry.tile_pos, self.entry.direction) def draw(self, surface): surface.blit(self.background, (0, 0)) self.sprites.draw(surface) def get_tile(self, tile_pos): x, y = tile_pos if 0 <= x < self.tile_size[0] and 0 <= y < self.tile_size[1]: return self.tiles[y][x] return None def replace_tile(self, tile_pos, new_tile_char): x, y = tile_pos old_tile = self.tiles[y][x] if old_tile is not None: old_tile.remove(self.sprites) new_tile = self.tileset.get_tile(new_tile_char, tile_pos, self.sprites) self.tiles[y][x] = new_tile # Fix orientations tiles = [self.get_tile((x + dx, y + dy)) for dx in (-1, 0, 1) for dy in (-1, 0, 1)] for tile in tiles: if tile: orientation = self.get_tile_orientation(tile) tile.use_variant(*orientation) def flip_arrows(self): for row in self.tiles: for tile in row: if isinstance(tile, sprites.ArrowSprite): tile.rotate() def restart(self): """Reset the level state""" self.setup_level(self.tiles_ascii, self.sprites_ascii) self.make_background() def get_sprite_at(self, sprite_pos): """Return the sprite line at the given pos, or none if it doesn't exist""" for sprite_ascii in self.sprites_ascii: try: pos, _, rest = sprite_ascii.partition(':') pos = [int(x.strip()) for x in pos.split(',')] except ValueError: continue if pos[0] == sprite_pos[0] and pos[1] == sprite_pos[1]: pos, _sep, rest = sprite_ascii.partition(':') class_name, rest = rest.split(None, 1) args = rest.split() sprite_cls = sprites.find_sprite(class_name) sprite_id, args = args[0], args[1:] return (class_name, sprite_cls, sprite_id, args), sprite_ascii return None, None def remove_sprite(self, sprite): """Remove the given sprite line from the list of sprites""" self.sprites_ascii.remove(sprite) def validate_sprite(self, sprite): """Check that the sprite line is valid""" try: pos, _sep, rest = sprite.partition(':') pos = [int(x.strip()) for x in pos.split(',')] class_name, rest = rest.split(None, 1) args = rest.split() except ValueError: raise InvalidMapError('Unable to determine sprite parameters.') sprite_id, args = args[0], args[1:] try: cls = sprites.find_sprite(class_name) except KeyError: raise InvalidMapError("Unknown Sprite class: %s" % class_name) sprite = cls(*args) if sprite_id in self.extra_sprites: # Check that duplicate id is not at the same position if self.extra_sprites[sprite_id].tile_pos != pos: raise InvalidMapError('Duplicate sprite id: %s.' % sprite_id) def add_sprite(self, sprite): self.sprites_ascii.append(sprite) def replace_sprite(self, new_ascii): # Need to find the sprite at the same psoition pos, _sep, rest = new_ascii.partition(':') pos = [int(x.strip()) for x in pos.split(',')] old_sprite, old_ascii = self.get_sprite_at(pos) self.sprites_ascii.remove(old_ascii) self.sprites_ascii.append(new_ascii)