view mamba/level.py @ 259:8d9424909118

Fix test of whether pygame.display is initialized.
author Simon Cross <hodgestar@gmail.com>
date Thu, 15 Sep 2011 01:43:52 +0200
parents 9827ce469834
children e461aa9d112f
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
from mamba.data import load_file
from mamba import sprites


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):
        self.level_name = level_name
        self.load_level_data()

    def load_level_data(self):
        """
        This file format is potentially yucky.
        """
        level_data = load_file('levels/%s.txt' % (self.level_name,))
        self.name = level_data.readline().strip()
        tileset_name = level_data.readline().strip()
        self.tileset = Tileset(tileset_name)
        tiles_ascii = [line.strip() for line in level_data.readlines()]
        end = tiles_ascii.index("end")
        sprites_ascii = tiles_ascii[end + 1:]
        tiles_ascii = tiles_ascii[:end]
        self.tiles_ascii = tiles_ascii
        self.sprites_ascii = sprites_ascii
        self.setup_level(tiles_ascii, sprites_ascii)
        self.make_background()

    def save_level(self):
        """Save the current state of the level"""
        save_file = load_file('levels/%s.txt' % (self.level_name,), 'wb')
        save_file.write('%s\n' % self.name)
        save_file.write('%s\n' % self.tileset.name)
        for tile_row in self.tiles:
            for tile in tile_row:
                if tile:
                    save_file.write(tile.tile_char)
                else:
                    save_file.write('.')
            save_file.write('\n')
        save_file.write('end\n')
        for sprite_ascii in self.sprites_ascii:
            save_file.write('%s\n' % sprite_ascii)

    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:
            pos, _sep, rest = sprite_ascii.partition(':')
            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()