changeset 47:fa351463d204

reorganized files
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Sat, 27 Aug 2011 23:18:10 +0200
parents 93ad08ed2a33
children 006f9df7bf25
files display.py effect.py generator.py hero.png hero.py level.png level.py lib/hero.png lib/level.png lib/monsters.png lib/pf_ronda_seven_bold.ttf lib/things.png main.py menu.py monster.py monsters.png pf_ronda_seven_bold.ttf src/display.py src/effect.py src/generator.py src/hero.py src/level.py src/main.py src/menu.py src/monster.py src/thing.py start thing.py things.png
diffstat 29 files changed, 1203 insertions(+), 1181 deletions(-) [+]
line wrap: on
line diff
--- a/display.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,217 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""A pygame helper that implements a scrolled layer with animated sprites."""
-
-import pygame
-import pygame.locals
-
-
-class GameQuit(Exception):
-    pass
-
-
-class TileCache(object):
-    """Load the tilesets lazily into 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 Sprite(pygame.sprite.Sprite):
-    def __init__(self, x, y, filename, tx=0, ty=0):
-        super(Sprite, self).__init__()
-        self.frames = sprites[filename]
-        self.image = self.frames[tx][ty]
-        self.rect = self.image.get_rect()
-        self.rect.move_ip(x * 24 - 4, y * 16 - 16)
-        self.depth = self.rect.midbottom[1]
-
-    def move(self, x, y):
-        self.rect.topleft = (x * 24 - 4, y * 16 - 16)
-        self.depth = self.rect.midbottom[1]
-
-class Overlay(pygame.sprite.Sprite):
-    def __init__(self, x, y, image):
-        super(Overlay, self).__init__()
-        self.image = image
-        self.rect = self.image.get_rect()
-        self.rect.move_ip(x * 24, y * 16)
-
-
-class SortedUpdates(pygame.sprite.RenderUpdates):
-    """Sprite group with a depth for controlling drawing order."""
-
-    def sprites(self):
-        return sorted(self.spritedict.keys(),
-                      key=lambda sprite: getattr(sprite, "depth", 0))
-
-
-class Display(object):
-    def __init__(self, size=(384, 384), caption=""):
-        pygame.display.init()
-        pygame.font.init()
-        pygame.display.set_mode(size)
-        pygame.display.set_caption(caption)
-        self.clock = pygame.time.Clock()
-        self.screen = pygame.display.get_surface()
-        self.sprites = SortedUpdates()
-        self.shadows = pygame.sprite.Group()
-        self.overlay = pygame.sprite.Group()
-        self.effects = pygame.sprite.RenderUpdates()
-        self.menus = SortedUpdates()
-        self.empty()
-
-    def empty(self):
-        self.key = None
-        self.scroll_offset = (0, 0)
-        self.background = pygame.Surface((self.screen.get_width(),
-                                          self.screen.get_height()))
-        self.background.fill((0, 0, 0))
-        self.sprites.empty()
-        self.shadows.empty()
-        self.overlay.empty()
-        self.effects.empty()
-
-    def _sprite_clear(self, screen, rect):
-        """Helper function for cleaning the sprites."""
-
-        scroll_x, scroll_y = self.scroll_offset
-        source = rect.move(-scroll_x, -scroll_y)
-        if not self.background.get_rect().contains(source):
-            screen.fill((0, 0, 0), rect)
-        screen.blit(self.background, rect, source)
-
-    def _sprite_shift(self, sprites, offset, reverse=False):
-        """Moves all the sprites in a group by an offset."""
-
-        dx, dy = offset
-        if reverse:
-            dx = -dx
-            dy = -dy
-        for sprite in sprites:
-            sprite.rect.move_ip(dx, dy)
-
-    def redraw(self):
-        """Redraw the whole screen. This is slow."""
-
-        self.screen.fill((0, 0, 0))
-        self.screen.blit(self.background, (0, 0),
-            pygame.rect.Rect(
-                (-self.scroll_offset[0], -self.scroll_offset[1]),
-                (self.screen.get_width(), self.screen.get_height())))
-        self.sprites.clear(self.screen, self._sprite_clear)
-        self.effects.clear(self.screen, self._sprite_clear)
-        self.menus.clear(self.screen, self._sprite_clear)
-        self.shadows.update()
-        self._sprite_shift(self.shadows, self.scroll_offset)
-        self._sprite_shift(self.sprites, self.scroll_offset)
-        self._sprite_shift(self.overlay, self.scroll_offset)
-        self._sprite_shift(self.effects, self.scroll_offset)
-        self.shadows.draw(self.screen)
-        self.sprites.draw(self.screen)
-        self.overlay.draw(self.screen)
-        self.effects.draw(self.screen)
-        self._sprite_shift(self.shadows, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.sprites, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.overlay, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.effects, self.scroll_offset, reverse=True)
-        self.menus.draw(self.screen)
-        pygame.display.flip()
-
-    def update(self):
-        """Update all the animated parts"""
-
-        self.sprites.clear(self.screen, self._sprite_clear)
-        self.effects.clear(self.screen, self._sprite_clear)
-        self.menus.clear(self.screen, self._sprite_clear)
-        self.sprites.update()
-        self.effects.update()
-        self.shadows.update()
-        self.menus.update()
-        self._sprite_shift(self.shadows, self.scroll_offset)
-        self._sprite_shift(self.sprites, self.scroll_offset)
-        self._sprite_shift(self.overlay, self.scroll_offset)
-        self._sprite_shift(self.effects, self.scroll_offset)
-        self.shadows.draw(self.screen)
-        dirty = self.sprites.draw(self.screen)
-        self.overlay.draw(self.screen)
-        dirty += self.effects.draw(self.screen)
-        self._sprite_shift(self.shadows, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.sprites, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.overlay, self.scroll_offset, reverse=True)
-        self._sprite_shift(self.effects, self.scroll_offset, reverse=True)
-        dirty += self.menus.draw(self.screen)
-        pygame.display.update(dirty)
-
-    def tick(self, tick=20):
-        """Handles a single frame of the game."""
-
-        self.update()
-        self.clock.tick(tick)
-        for event in pygame.event.get():
-            if event.type == pygame.locals.QUIT:
-                raise GameQuit()
-            elif event.type == pygame.locals.KEYDOWN:
-                self.key = event.key
-
-    def get_pressed(self):
-        """Get the array of pressed keys."""
-
-        keys = pygame.key.get_pressed()
-        keys = set(key for (key, pressed) in enumerate(keys)
-                   if pressed or key == self.key)
-        self.key = None
-        return keys
-
-    def scroll_to(self, rect):
-        self.scroll_offset = (self.screen.get_width() / 2 - rect.center[0],
-                              self.screen.get_height() / 2 - rect.center[1])
-        self.redraw()
-
-    def scroll_after(self, rect):
-        x, y = self.scroll_offset
-        hscroll, vscroll = 0, 0
-        if rect.left < 48-x:
-            hscroll = 3
-        if rect.right > self.screen.get_width()-48-x:
-            hscroll = -3
-        if rect.top < 48-y:
-            vscroll = 2
-        if rect.bottom > self.screen.get_height()-48-y:
-            vscroll = -2
-        if hscroll or vscroll:
-            self.scroll_offset = x + hscroll, y + vscroll
-            self.redraw()
-
-
-sprites = TileCache()
-tiles = TileCache(24, 16)
--- a/effect.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import pygame
-
-
-def get_font():
-    return pygame.font.Font('pf_ronda_seven_bold.ttf', 8)
-
-def outlined_text(text, color, border_color):
-    font = get_font()
-    notcolor = [c^0xFF for c in border_color]
-    base = font.render(text, 0, border_color, notcolor)
-    size = base.get_width() + 2, base.get_height() + 2
-    image = pygame.Surface(size, 16)
-    image.fill(notcolor)
-    base.set_colorkey(0)
-    image.blit(base, (0, 0))
-    image.blit(base, (2, 0))
-    image.blit(base, (0, 2))
-    image.blit(base, (2, 2))
-    base.set_colorkey(0)
-    base.set_palette_at(1, color)
-    image.blit(base, (1, 1))
-    image.set_colorkey(notcolor)
-    return image
-
-class CaptionSprite(pygame.sprite.Sprite):
-    def __init__(self, x, y, text, color, delay, ttl):
-        super(CaptionSprite, self).__init__()
-        self.text = text
-        self.color = color
-        bgcolor = (32, 32, 32)
-        self.image = outlined_text(text, color, bgcolor)
-        self.rect = self.image.get_rect()
-        self.x = x
-        self.y = y
-        self.rect.center = (x * 24 + 12, y * 16 + 8 - 24)
-        self.ttl = ttl
-        self.delay = delay
-
-    def update(self):
-        if self.delay:
-            self.delay -= 1
-            self.image.set_alpha(max(0, 255 - self.delay * 24))
-            return
-        self.rect.move_ip(0, -2)
-        self.ttl -= 1
-        if self.ttl < 5:
-            self.image.set_alpha(self.ttl * 51)
-        if self.ttl <= 0:
-            self.kill()
-
-class Caption(object):
-    def __init__(self, x, y, text, color=(255, 255, 255), ttl=16):
-        self.x = x
-        self.y = y
-        self.text = text
-        self.color = color
-        self.ttl = ttl
-
-    def render(self, to_display, delay=0):
-        self.sprite = CaptionSprite(self.x, self.y, self.text,
-                                    self.color, delay, self.ttl)
-        to_display.effects.add(self.sprite)
-
--- a/generator.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import random
-import thing
-import monster
-
-
-class LevelGenerator(object):
-    depth_monsters = [
-        [monster.Bat],
-        [monster.Bat, monster.Skeleton],
-        [monster.Bat, monster.Skeleton, monster.Zombie],
-        [monster.Skeleton, monster.Zombie],
-        [monster.Skeleton, monster.Zombie, monster.Jelly],
-        [monster.Skeleton, monster.Zombie, monster.Jelly, monster.Ghost],
-        [monster.Jelly, monster.Zombie, monster.Ghost],
-        [monster.Jelly, monster.Ghost, monster.Jack],
-        [monster.Ghost, monster.Jack],
-        [monster.Ghost, monster.Jack, monster.Eye],
-        [monster.Jack, monster.Eye],
-        [monster.Bat, monster.Jack, monster.Eye],
-        [monster.Bat, monster.Skeleton, monster.Zombie, monster.Jack,
-         monster.Eye, monster.Jelly, monster.Ghost],
-    ]
-
-    def __init__(self, depth):
-        self.depth = depth
-        self.map = [['x'] * 30 for i in range(30)]
-        self.things = {}
-        self.monsters = {}
-        self.rooms = [[self.make_room(x, y) for x in range(3)]
-                      for y in range(3)]
-        for line in self.rooms:
-            for x1, y1, x2, y2 in line:
-                self.rect(x1, y1, x2, y2, 'X', '.+:')
-        self.make_connections()
-        for line in self.map:
-            for i, c in enumerate(line):
-                if c == 'X':
-                    line[i] = 'x'
-        self.make_monsters()
-        self.make_trash()
-        self.make_stairs()
-
-    def _random_place(self):
-        r1 = random.choice(random.choice(self.rooms))
-        x1 = random.randint(r1[0] + 1, r1[2] - 1)
-        y1 = random.randint(r1[1] + 1, r1[3] - 1)
-        return x1, y1
-
-    def make_trash(self):
-        trash = [thing.Plant, thing.Chest]
-        for i in range(random.randint(5, 15)):
-            p = self._random_place()
-            if p not in self.things:
-                self.things[p] = random.choice(trash)(*p)
-
-    def make_monsters(self):
-        for i in range(15):
-            p = self._random_place()
-            if (p not in self.monsters and
-                not isinstance(self.things.get(p), thing.Entrance)):
-                monsters = self.depth_monsters[self.depth]
-                self.monsters[p] = random.choice(monsters)(*p)
-
-    def make_stairs(self):
-        p = self._random_place()
-        while p in self.things:
-            p = self._random_place()
-        self.things[p] = thing.Entrance(*p)
-        p = self._random_place()
-        while p in self.things:
-            p = self._random_place()
-        x1, y1 = p
-        self.map[y1][x1] = '>'
-
-    def make_connections(self):
-        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
-        connections = []
-        connected = [(random.randint(0, 2), random.randint(0, 2))]
-        while len(connected) < 9:
-            x1, y1 = random.choice(connected)
-            try:
-                dx, dy = random.choice([(dx, dy) for (dx, dy)
-                    in dirs if 0 <= x1 + dx <= 2 and 0 <= y1 + dy <= 2 and
-                        (x1 + dx, y1 + dy) not in connected])
-            except IndexError:
-                # pick another room
-                continue
-            r1 = self.rooms[y1][x1]
-            x2, y2 = x1 + dx, y1 + dy
-            r2 = self.rooms[y2][x2]
-            self.connect_rooms(r1, r2)
-            connections.append((x1, y1, x2, y2))
-            connected.append((x2, y2))
-        for loop in range(3):
-            x1 = random.randint(0, 2)
-            y1 = random.randint(0, 2)
-            possible = [(dx, dy) for (dx, dy)
-                in dirs if 0 <= x1 + dx <= 2 and 0 <= y1 + dy <= 2 and
-                not ((x1, y1, x1 + dx, y1 + dy) in connections or
-                     (x1 + dx, y1 + dy, x1, y1) in connections)]
-            if not possible:
-                continue
-            dx, dy = random.choice(possible)
-            x2, y2 = x1 + dx, y1 + dy
-            r1 = self.rooms[y1][x1]
-            r2 = self.rooms[y2][x2]
-            self.connect_rooms(r1, r2)
-            connections.append((x1, y1, x2, y2))
-
-    def make_room(self, x1, y1):
-        w = random.randint(3, 8)
-        h = random.randint(3, 8)
-        x = 10 * x1 + random.randint(0, 8 - w)
-        y = 10 * y1 + random.randint(0, 8 - h)
-        return (x, y, x + w, y + h)
-
-    def connect_rooms(self, r1, r2):
-        if abs(r1[0] - r2[0]) < abs(r1[1] - r2[1]):
-            # vertical
-            v = True
-            if r1[1] > r2[1]:
-                r1, r2 = r2, r1
-            x1 = random.randint(r1[0] + 1, r1[2] - 1)
-            y1 = r1[3]
-            x2 = random.randint(r2[0] + 1, r2[2] - 1)
-            y2 = r2[1]
-            if y1 + 1 != y2 - 1:
-                m = random.randint(y1 + 1, y2 - 1)
-            else:
-                m = y1 + 1
-            for y in range(y1, m):
-                self.map[y][x1] = random.choice(',;')
-            if x1 < x2:
-                for x in range(x1, x2 + 1):
-                    self.map[m][x] = random.choice(';"')
-            else:
-                for x in range(x2, x1 + 1):
-                    self.map[m][x] = random.choice(';"')
-            for y in range(m, y2):
-                self.map[y][x2] = random.choice(',;')
-        else:
-            # horizontal
-            v = False
-            if r1[0] > r2[0]:
-                r1, r2 = r2, r1
-            x1 = r1[2]
-            y1 = random.randint(r1[1] + 1, r1[3] - 1)
-            x2 = r2[0]
-            y2 = random.randint(r2[1] + 1, r2[3] - 1)
-            if x1 + 1 != x2 - 1:
-                m = random.randint(x1 + 1, x2 - 1)
-            else:
-                m = x1 + 1
-            for x in range(x1, m):
-                self.map[y1][x] = random.choice(',;')
-            if y1 < y2:
-                for y in range(y1, y2 + 1):
-                    self.map[y][m] = random.choice(';"')
-            else:
-                for y in range(y2, y1 + 1):
-                    self.map[y][m] = random.choice(';"')
-            for x in range(m, x2):
-                self.map[y2][x] = random.choice(',;')
-        self.map[y1][x1] = '+'
-        self.map[y2][x2] = '+'
-        if random.randint(0, 10) > 7:
-            self.things[(x1, y1)] = thing.Door(x1, y1, not v)
-        if random.randint(0, 10) > 7:
-            self.things[(x2, y2)] = thing.Door(x2, y2, not v)
-
-    def rect(self, x1, y1, x2, y2, c, fill=None):
-        if fill:
-            for y in range(y1 + 1, y2):
-                for x in range(x1 + 1, x2):
-                    self.map[y][x] = random.choice(fill)
-        for y in range(y1, y2 + 1):
-            self.map[y][x1] = c
-            self.map[y][x2] = c
-        for x in range(x1, x2 + 1):
-            self.map[y1][x] = c
-            self.map[y2][x] = c
Binary file hero.png has changed
--- a/hero.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import display
-import thing
-import effect
-
-
-class HeroSprite(display.Sprite):
-    faces = {
-        -1: { -1: 1, 0: 1, 1: 0 },
-         0: { -1: 2, 0: 0, 1: 0 },
-         1: { -1: 2, 0: 3, 1: 3},
-    }
-
-    def __init__(self):
-        super(HeroSprite, self).__init__(0, 0, 'hero.png')
-        self.facing = 2
-        self.animation = None
-
-    def update(self):
-        if self.animation:
-            try:
-                self.animation.next()
-            except StopIteration:
-                self.animation = None
-        else:
-            self.image = self.frames[self.facing][0]
-
-    def anim_walk(self, dx, dy):
-        self.facing = self.faces[dx][dy]
-        self.depth = self.rect.midbottom[1] + 1
-        for frame in range(4):
-            self.image = self.frames[self.facing][frame % 4]
-            for step in range(2):
-                yield
-                self.rect.move_ip(dx * 3, dy * 2)
-                self.depth = self.rect.midbottom[1] + 1
-
-    def anim_blocked(self, dx, dy):
-        self.facing = self.faces[dx][dy]
-        self.depth = self.rect.midbottom[1] + 1
-        for frame in range(4):
-            yield
-            self.image = self.frames[self.facing][frame % 4]
-            yield
-
-    def anim_enter(self):
-        self.depth = self.rect.midbottom[1] + 1
-        self.image = self.frames[2][0]
-        self.rect.move_ip(0, -15)
-        for step in range(6):
-            yield
-            self.rect.move_ip(0, step)
-
-    def anim_exit(self):
-        self.depth = self.rect.midbottom[1] + 1
-        image = self.frames[2][0].copy()
-        self.image = image
-        move = 0
-        for step in range(6):
-            yield
-            move += step
-            self.image = image.subsurface(0, 0, 32, 32 - move)
-            self.rect.move_ip(0, step)
-
-    def anim_fight(self, dx, dy):
-        self.facing = self.faces[dx][dy]
-        self.image = self.frames[self.facing][4]
-        h = [1, 2, -3]
-        for frame in range(3):
-            yield
-            self.rect.move_ip(dx * 3 * frame, dy * 2 * frame - h[frame])
-            self.depth = self.rect.midbottom[1] + 1 + h[frame]
-        self.depth = self.rect.midbottom[1] + 1
-        for frame in range(3):
-            yield
-            self.image = self.frames[self.facing][1]
-            self.rect.move_ip(-dx * 3 * frame, -dy * 2 * frame)
-            self.depth = self.rect.midbottom[1] + 1
-        self.image = self.frames[self.facing][0]
-        for frame in range(2):
-            yield
-
-
-class Hero(thing.Thing):
-    def __init__(self, x=0, y=0):
-        self.x, self.y = x, y
-        self.effects = []
-        self.sprite = HeroSprite()
-        self.max_hp = 10
-        self.hp = self.max_hp
-        self.regen = 10
-        self.regen_tick = 0
-
-    def render(self, to_display):
-        to_display.sprites.add(self.sprite)
-        to_display.sprites.add(thing.ShadowSprite(self.sprite))
-
-    def tick(self):
-        self.regen_tick += 1
-        if self.regen_tick > self.regen:
-            if self.hp < self.max_hp:
-                self.hp += 1
-                self.effects.append(
-                    effect.Caption(self.x, self.y, '+1', (32, 128, 0)))
-            self.regen_tick = 0
-
-    def enter(self, level, x, y):
-        self.x = x
-        self.y = y
-        self.sprite.move(x, y)
-        self.sprite.facing = 2
-        self.sprite.animation = self.sprite.anim_enter()
-
-    def exit(self, level):
-        self.sprite.animation = self.sprite.anim_exit()
-        level.hero = None
-
-    def walk(self, level, dx, dy):
-        block = level.is_blocked(self.x + dx, self.y + dy)
-        if not block:
-            self.x += dx
-            self.y += dy
-            self.sprite.animation = self.sprite.anim_walk(dx, dy)
-        elif hasattr(block, 'hit'):
-            self.attack(level, dx, dy, block)
-        else:
-            self.sprite.animation = self.sprite.anim_blocked(dx, dy)
-        self.sprite.animation.next()
-
-    def attack(self, level, dx, dy, what=None):
-        if what is None:
-            what = level.is_blocked(self.x + dx, self.y + dy)
-        if not hasattr(what, 'hit'):
-            return
-        what.hit()
-        self.sprite.animation = self.sprite.anim_fight(dx, dy)
-        if isinstance(what, thing.Chest):
-            self.effects.append(
-                effect.Caption(self.x + dx, self.y + dy, 'empty', (64, 192, 0)))
-
-    def hit(self, damage):
-        self.hp -= damage
-        self.regen_tick = 0
-        self.effects.append(
-            effect.Caption(self.x, self.y, '%d' % (-damage), (255, 32, 32)))
-
Binary file level.png has changed
--- a/level.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import display
-import thing
-import generator
-import pygame
-
-
-class Tile(object):
-    filename = 'level.png'
-    tx, ty = 0, 3
-    blocks = False
-
-    def __init__(self, x, y, map):
-        self.x = x
-        self.y = y
-        self.map = map
-
-    def render(self, to_display):
-        tile = display.tiles[self.filename][self.tx][self.ty]
-        to_display.background.blit(tile, (self.x * 24, self.y * 16))
-
-
-class BrokenTile(Tile):
-    tx, ty = 2, 3
-
-
-class GroundTile(Tile):
-    tx, ty = 0, 4
-
-
-class PlantsTile(Tile):
-    tx, ty = 1, 4
-
-
-class WeedTile(Tile):
-    tx, ty = 2, 4
-
-
-class GrassTile(Tile):
-    tx, ty = 3, 4
-
-
-class DoorTile(Tile):
-    tx, ty = 3, 3
-
-
-class StairsTile(Tile):
-    tx, ty = 1, 3
-
-
-class WallTile(Tile):
-    blocks = True
-
-    def render(self, to_display):
-        map = self.map
-        x = self.x
-        y = self.y
-        c = map[y][x]
-        tileset = display.tiles[self.filename]
-        def f(x, y):
-            return map[y][x] == c
-        def sides(x, y):
-            if y < 0:
-                return 3
-            if x <= 0 or f(x - 1, y):
-                if x >= len(map[y])-1 or f(x + 1, y):
-                    return 1
-                else:
-                    return 2
-            elif x >= len(map[y])-1 or f(x + 1, y):
-                return 0
-            return 3
-        if y > 0 and not f(x, y - 1):
-            overlay = display.Overlay(x, y - 1, tileset[sides(x, y)][0])
-            to_display.overlay.add(overlay)
-        if y < len(map) - 1:
-            if f(x, y + 1):
-                tile = tileset[sides(x, y + 1)][1]
-            else:
-                tile = tileset[sides(x, y)][2]
-            to_display.background.blit(tile, (self.x * 24, self.y * 16))
-
-
-class Level(object):
-    key = {
-        '.': Tile,
-        '+': DoorTile,
-        ':': BrokenTile,
-        '>': StairsTile,
-        'x': WallTile,
-        ',': GroundTile,
-        ';': PlantsTile,
-        '"': WeedTile,
-        '*': GrassTile,
-    }
-    BLOCKED = 1
-
-    def __init__(self, hero, depth):
-        self.hero = hero
-        self.depth = depth
-        gen = generator.LevelGenerator(depth)
-        self.map = gen.map
-        self.things = {}
-        for t in gen.things.itervalues():
-            self.things[(t.x, t.y)] = t
-            if isinstance(t, thing.Entrance):
-                self.hero.enter(self, t.x, t.y)
-        self.monsters = {}
-        for m in gen.monsters.itervalues():
-            self.monsters[(m.x, m.y)] = m
-
-    def is_blocked(self, x, y):
-        if (x, y) in self.monsters:
-            return self.monsters[(x, y)]
-        t = self.things.get((x, y))
-        if t and t.blocks:
-            return t
-        if x<0 or y<0:
-            return True
-        try:
-            return self.flags[y][x] & self.BLOCKED
-        except IndexError:
-            return True
-
-    def render(self, display):
-        display.background = pygame.Surface((len(self.map[0]) * 24,
-                                             len(self.map) * 16))
-        display.background.fill((0, 0, 0))
-        self.flags = []
-        for y, line in enumerate(self.map):
-            self.flags.append([])
-            for x, c in enumerate(line):
-                tile_class = self.key.get(c)
-                if tile_class:
-                    tile = tile_class(x, y, self.map)
-                    tile.render(display)
-                self.flags[-1].append(0)
-                if tile_class is None or tile.blocks:
-                    self.flags[-1][-1] |= self.BLOCKED
-        self.hero.render(display)
-        for t in self.things.itervalues():
-            t.render(display)
-        for m in self.monsters.itervalues():
-            m.render(display)
Binary file lib/hero.png has changed
Binary file lib/level.png has changed
Binary file lib/monsters.png has changed
Binary file lib/pf_ronda_seven_bold.ttf has changed
Binary file lib/things.png has changed
--- a/main.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import pygame.locals
-
-import display
-import level
-import hero
-import menu
-
-class Game(object):
-    def __init__(self):
-        self.depth = 0
-        self.display = display.Display((320, 200))
-        self.hero = hero.Hero()
-        self.start_level()
-        self.health = menu.GameInfo(288, 8, 'HP %d/10' % self.hero.hp,
-                                    (32, 192, 0))
-        self.display.menus.add(self.health)
-
-    def start_level(self):
-        self.display.empty()
-        self.level = level.Level(self.hero, self.depth)
-        self.depth += 1
-        self.level.render(self.display)
-        self.display.scroll_to(self.hero.sprite.rect)
-
-    def control(self):
-        if self.display.key == pygame.locals.K_ESCAPE:
-            raise display.GameQuit()
-        end_turn = False
-        if not self.hero.sprite.animation:
-            if self.level.hero is None:
-                self.start_level()
-            if self.level.map[self.hero.y][self.hero.x] == '>':
-                self.hero.exit(self.level)
-                return
-            pressed = self.display.get_pressed()
-            if pygame.locals.K_UP in pressed:
-                self.hero.walk(self.level, 0, -1)
-                end_turn = True
-            elif pygame.locals.K_DOWN in pressed:
-                self.hero.walk(self.level, 0, 1)
-                end_turn = True
-            elif pygame.locals.K_LEFT in pressed:
-                self.hero.walk(self.level, -1, 0)
-                end_turn = True
-            elif pygame.locals.K_RIGHT in pressed:
-                self.hero.walk(self.level, 1, 0)
-                end_turn = True
-        if end_turn:
-            self.hero.tick()
-            self.health.change('HP %d/10' % self.hero.hp)
-            monsters = self.level.monsters.values()
-            for m in monsters:
-                if m.dead:
-                    del self.level.monsters[(m.x, m.y)]
-            for m in monsters:
-                if not m.dead:
-                    m.act(self.level)
-            for m in monsters + [self.hero]:
-                for d, effect in enumerate(m.effects):
-                    effect.render(self.display, delay=d * 12)
-                m.effects = []
-
-    def main(self):
-        try:
-            while True:
-                self.display.tick(tick=24)
-                self.control()
-                self.display.scroll_after(self.hero.sprite.rect)
-        except display.GameQuit:
-            pass
-
-if __name__ == '__main__':
-    Game().main()
--- a/menu.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import pygame
-import effect
-
-
-class GameInfo(pygame.sprite.Sprite):
-    def __init__(self, x, y, text, color):
-        super(GameInfo, self).__init__()
-        self.x, self.y = x, y
-        self.text = text
-        self.color = color
-        self.image = effect.outlined_text(text, color, (32, 32, 32))
-        self.rect = self.image.get_rect()
-        self.rect.center = self.x, self.y
-
-    def change(self, text=None, color=None):
-        if text is not None:
-            self.text = text
-        if color is not None:
-            self.color = color
-        self.image = effect.outlined_text(self.text, self.color, (32, 32, 32))
-        self.rect = self.image.get_rect()
-        self.rect.center = self.x, self.y
--- a/monster.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,233 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import random
-
-import thing
-import effect
-import display
-
-
-dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
-
-
-def distance(x1, y1, x2, y2):
-    return abs(x1 - x2) + abs(y1 - y2)
-
-
-class MonsterSprite(display.Sprite):
-    def __init__(self, x, y, tx):
-        self.tx = tx
-        self.ty = 0
-        super(MonsterSprite, self).__init__(x, y, 'monsters.png')
-        self.animation = self.anim_default()
-        self.frame = 0
-
-    def update(self):
-        try:
-            self.animation.next()
-        except StopIteration:
-            self.animation = self.anim_default()
-            self.animation.next()
-
-    def anim_default(self):
-        self.depth = self.rect.midbottom[1] + 1
-        while True:
-            yield
-            self.frame = (self.frame + 1) % 4
-            self.image = self.frames[self.tx][self.frame % 4]
-            yield
-            yield
-
-    def anim_walk(self, dx, dy):
-        for frame in range(2):
-            self.frame = (self.frame + 1) % 4
-            for step in range(4):
-                yield
-                self.image = self.frames[self.tx][self.frame]
-                self.rect.move_ip(dx * 3, dy * 2)
-                self.depth = self.rect.midbottom[1] + 1
-
-    def anim_fight(self, dx, dy):
-        self.image = self.frames[self.tx][1]
-        h = [1, 2, -3]
-        latitude = 0
-        for frame in range(2):
-            yield
-        for frame in range(3):
-            yield
-            self.rect.move_ip(dx * 3 * frame, dy * 2 * frame - h[frame])
-            latitude += h[frame]
-            self.depth = self.rect.midbottom[1] - latitude + 1
-        self.depth = self.rect.midbottom[1]
-        for frame in range(3):
-            yield
-            self.image = self.frames[self.tx][1]
-            self.rect.move_ip(-dx * 3 * frame, -dy * 2 * frame)
-            self.depth = self.rect.midbottom[1] + 1
-        self.image = self.frames[self.tx][0]
-
-    def anim_die(self):
-        image = self.frames[self.tx][0]
-        edge = 4
-        yield
-        yield
-        for frame in range(8):
-            yield
-            self.image = image.subsurface(0, 0, 32, 32 - edge)
-            edge += frame
-            self.image.set_alpha(256 - frame * 32)
-            self.rect.move_ip(0, frame - 1)
-        self.kill()
-
-
-class Monster(thing.Thing):
-    tx = 0
-    max_hp = 4
-    name = "monster"
-    alpha = None
-
-    def __init__(self, x, y):
-        self.x, self.y = x, y
-        self.dead = False
-        self.effects = []
-        self.hp = self.max_hp
-        self.asleep = False
-
-    def render(self, to_display):
-        self.sprite = MonsterSprite(self.x, self.y, self.tx)
-        if self.alpha:
-            for frame in self.sprite.frames[self.tx]:
-                frame.set_alpha(self.alpha)
-        to_display.sprites.add(self.sprite)
-        to_display.sprites.add(thing.ShadowSprite(self.sprite))
-
-    def hit(self, damage=1):
-        self.asleep = False
-        self.effects.append(
-            effect.Caption(self.x, self.y, '%d' % (-damage), (255, 128, 16)))
-        self.hp -= damage
-        if self.hp <= 0:
-            self.die()
-
-    def die(self):
-        c = effect.Caption(self.x, self.y, self.name, (192, 192, 192), ttl=8)
-        self.effects.append(c)
-        self.dead = True
-        self.sprite.animation = self.sprite.anim_die()
-        self.sprite.animation.next()
-
-    def act(self, level):
-        # Attack if standing next to the hero
-        if distance(level.hero.x, level.hero.y, self.x, self.y) == 1:
-            self.attack(level, level.hero.x - self.x, level.hero.y - self.y)
-            return
-
-        if self.asleep:
-            return
-
-        # Walk towards the hero
-        possible = [(dx, dy) for (dx, dy) in dirs if
-                    not self.is_blocked(level, dx, dy)]
-        if not possible:
-            return
-        random.shuffle(possible)
-        dx, dy = min(possible,
-            key=lambda d: distance(level.hero.x, level.hero.y,
-                                        self.x+d[0], self.y+d[1]))
-        self.walk(level, dx, dy)
-
-    def is_blocked(self, level, dx, dy):
-        block = level.is_blocked(self.x + dx, self.y + dy)
-        if block:
-            return True
-
-    def walk(self, level, dx, dy):
-        if self.is_blocked(level, dx, dy):
-            return
-        if (level.hero.x, level.hero.y) == (self.x + dx, self.y + dy):
-            self.attack(level, dx, dy)
-            return
-        del level.monsters[(self.x, self.y)]
-        self.x += dx
-        self.y += dy
-        level.monsters[(self.x, self.y)] = self
-        self.sprite.animation = self.sprite.anim_walk(dx, dy)
-        self.sprite.animation.next()
-
-    def attack(self, level, dx, dy):
-        self.asleep = False
-        if not (level.hero.x, level.hero.y) == (self.x + dx, self.y + dy):
-            return
-        self.sprite.animation = self.sprite.anim_fight(dx, dy)
-        self.sprite.animation.next()
-        level.hero.hit(1)
-
-
-class Skeleton(Monster):
-    tx = 0
-    name = "skeleton"
-    max_hp = 4
-
-
-class Zombie(Monster):
-    tx = 1
-    name = "zombie"
-    max_hp = 7
-
-
-class Jack(Monster):
-    tx = 2
-    name = "jack"
-    max_hp = 8
-
-
-class Bat(Monster):
-    tx = 3
-    name = "bat"
-    max_hp = 2
-
-    def act(self, level):
-        # Attack if we feel like it
-        if random.choice([True, False]):
-            if distance(level.hero.x, level.hero.y, self.x, self.y) == 1:
-                self.attack(level, level.hero.x - self.x, level.hero.y - self.y)
-                return
-
-        if self.asleep:
-            return
-
-        # Move randomly (very annoying)
-        possible = [(dx, dy) for (dx, dy) in dirs if
-                    not self.is_blocked(level, dx, dy)]
-        if not possible:
-            return
-        dx, dy = random.choice(possible)
-        self.walk(level, dx, dy)
-
-
-class Ghost(Monster):
-    tx = 4
-    name = "ghost"
-    max_hp = 5
-    alpha = 96
-
-    def is_blocked(self, level, dx, dy):
-        # Ghosts can pass through obstacles
-        block = level.is_blocked(self.x + dx, self.y + dy)
-        if block and isinstance(block, Monster) or block == True:
-            return True
-
-
-class Eye(Monster):
-    tx = 5
-    name = "eye"
-    max_hp = 7
-    asleep = True
-
-
-class Jelly(Monster):
-    tx = 6
-    name = "jelly"
-    max_hp = 6
-    alpha = 192
Binary file monsters.png has changed
Binary file pf_ronda_seven_bold.ttf has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/display.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,221 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""A pygame helper that implements a scrolled layer with animated sprites."""
+
+import os
+import pygame
+import pygame.locals
+
+
+class GameQuit(Exception):
+    pass
+
+
+class TileCache(object):
+    """Load the tilesets lazily into cache."""
+
+    def __init__(self,  width=32, height=None):
+        self.dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                                '../lib')
+        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(os.path.join(self.dir, 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 Sprite(pygame.sprite.Sprite):
+    def __init__(self, x, y, filename, tx=0, ty=0):
+        super(Sprite, self).__init__()
+        self.frames = sprites[filename]
+        self.image = self.frames[tx][ty]
+        self.rect = self.image.get_rect()
+        self.rect.move_ip(x * 24 - 4, y * 16 - 16)
+        self.depth = self.rect.midbottom[1]
+
+    def move(self, x, y):
+        self.rect.topleft = (x * 24 - 4, y * 16 - 16)
+        self.depth = self.rect.midbottom[1]
+
+class Overlay(pygame.sprite.Sprite):
+    def __init__(self, x, y, image):
+        super(Overlay, self).__init__()
+        self.image = image
+        self.rect = self.image.get_rect()
+        self.rect.move_ip(x * 24, y * 16)
+
+
+class SortedUpdates(pygame.sprite.RenderUpdates):
+    """Sprite group with a depth for controlling drawing order."""
+
+    def sprites(self):
+        return sorted(self.spritedict.keys(),
+                      key=lambda sprite: getattr(sprite, "depth", 0))
+
+
+class Display(object):
+    def __init__(self, size=(384, 384), caption=""):
+        pygame.display.init()
+        pygame.display.set_mode(size)
+        pygame.display.set_caption(caption)
+        self.clock = pygame.time.Clock()
+        self.screen = pygame.display.get_surface()
+        self.sprites = SortedUpdates()
+        self.shadows = pygame.sprite.Group()
+        self.overlay = pygame.sprite.Group()
+        self.effects = pygame.sprite.RenderUpdates()
+        self.menus = SortedUpdates()
+        self.empty()
+
+    def empty(self):
+        self.key = None
+        self.scroll_offset = (0, 0)
+        self.background = pygame.Surface((self.screen.get_width(),
+                                          self.screen.get_height()))
+        self.background.fill((0, 0, 0))
+        self.sprites.empty()
+        self.shadows.empty()
+        self.overlay.empty()
+        self.effects.empty()
+
+    def _sprite_clear(self, screen, rect):
+        """Helper function for cleaning the sprites."""
+
+        scroll_x, scroll_y = self.scroll_offset
+        source = rect.move(-scroll_x, -scroll_y)
+        if not self.background.get_rect().contains(source):
+            screen.fill((0, 0, 0), rect)
+        screen.blit(self.background, rect, source)
+
+    def _sprite_shift(self, sprites, offset, reverse=False):
+        """Moves all the sprites in a group by an offset."""
+
+        dx, dy = offset
+        if reverse:
+            dx = -dx
+            dy = -dy
+        for sprite in sprites:
+            sprite.rect.move_ip(dx, dy)
+
+    def redraw(self):
+        """Redraw the whole screen. This is slow."""
+
+        self.screen.fill((0, 0, 0))
+        self.screen.blit(self.background, (0, 0),
+            pygame.rect.Rect(
+                (-self.scroll_offset[0], -self.scroll_offset[1]),
+                (self.screen.get_width(), self.screen.get_height())))
+        self.sprites.clear(self.screen, self._sprite_clear)
+        self.effects.clear(self.screen, self._sprite_clear)
+        self.menus.clear(self.screen, self._sprite_clear)
+        self.shadows.update()
+        self._sprite_shift(self.shadows, self.scroll_offset)
+        self._sprite_shift(self.sprites, self.scroll_offset)
+        self._sprite_shift(self.overlay, self.scroll_offset)
+        self._sprite_shift(self.effects, self.scroll_offset)
+        self.shadows.draw(self.screen)
+        self.sprites.draw(self.screen)
+        self.overlay.draw(self.screen)
+        self.effects.draw(self.screen)
+        self._sprite_shift(self.shadows, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.sprites, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.overlay, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.effects, self.scroll_offset, reverse=True)
+        self.menus.draw(self.screen)
+        pygame.display.flip()
+
+    def update(self):
+        """Update all the animated parts"""
+
+        self.sprites.clear(self.screen, self._sprite_clear)
+        self.effects.clear(self.screen, self._sprite_clear)
+        self.menus.clear(self.screen, self._sprite_clear)
+        self.sprites.update()
+        self.effects.update()
+        self.shadows.update()
+        self.menus.update()
+        self._sprite_shift(self.shadows, self.scroll_offset)
+        self._sprite_shift(self.sprites, self.scroll_offset)
+        self._sprite_shift(self.overlay, self.scroll_offset)
+        self._sprite_shift(self.effects, self.scroll_offset)
+        self.shadows.draw(self.screen)
+        dirty = self.sprites.draw(self.screen)
+        self.overlay.draw(self.screen)
+        dirty += self.effects.draw(self.screen)
+        self._sprite_shift(self.shadows, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.sprites, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.overlay, self.scroll_offset, reverse=True)
+        self._sprite_shift(self.effects, self.scroll_offset, reverse=True)
+        dirty += self.menus.draw(self.screen)
+        pygame.display.update(dirty)
+
+    def tick(self, tick=20):
+        """Handles a single frame of the game."""
+
+        self.update()
+        self.clock.tick(tick)
+        for event in pygame.event.get():
+            if event.type == pygame.locals.QUIT:
+                raise GameQuit()
+            elif event.type == pygame.locals.KEYDOWN:
+                self.key = event.key
+
+    def get_pressed(self):
+        """Get the array of pressed keys."""
+
+        keys = pygame.key.get_pressed()
+        keys = set(key for (key, pressed) in enumerate(keys)
+                   if pressed or key == self.key)
+        self.key = None
+        return keys
+
+    def scroll_to(self, rect):
+        self.scroll_offset = (self.screen.get_width() / 2 - rect.center[0],
+                              self.screen.get_height() / 2 - rect.center[1])
+        self.redraw()
+
+    def scroll_after(self, rect):
+        x, y = self.scroll_offset
+        hscroll, vscroll = 0, 0
+        if rect.left < 48-x:
+            hscroll = 3
+        if rect.right > self.screen.get_width()-48-x:
+            hscroll = -3
+        if rect.top < 48-y:
+            vscroll = 2
+        if rect.bottom > self.screen.get_height()-48-y:
+            vscroll = -2
+        if hscroll or vscroll:
+            self.scroll_offset = x + hscroll, y + vscroll
+            self.redraw()
+
+
+sprites = TileCache(32, 32)
+tiles = TileCache(24, 16)
+pygame.font.init()
+font = pygame.font.Font(os.path.join(tiles.dir, 'pf_ronda_seven_bold.ttf'), 8)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/effect.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,64 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import pygame
+import display
+
+
+def outlined_text(text, color, border_color):
+    font = display.font
+    notcolor = [c^0xFF for c in border_color]
+    base = font.render(text, 0, border_color, notcolor)
+    size = base.get_width() + 2, base.get_height() + 2
+    image = pygame.Surface(size, 16)
+    image.fill(notcolor)
+    base.set_colorkey(0)
+    image.blit(base, (0, 0))
+    image.blit(base, (2, 0))
+    image.blit(base, (0, 2))
+    image.blit(base, (2, 2))
+    base.set_colorkey(0)
+    base.set_palette_at(1, color)
+    image.blit(base, (1, 1))
+    image.set_colorkey(notcolor)
+    return image
+
+class CaptionSprite(pygame.sprite.Sprite):
+    def __init__(self, x, y, text, color, delay, ttl):
+        super(CaptionSprite, self).__init__()
+        self.text = text
+        self.color = color
+        bgcolor = (32, 32, 32)
+        self.image = outlined_text(text, color, bgcolor)
+        self.rect = self.image.get_rect()
+        self.x = x
+        self.y = y
+        self.rect.center = (x * 24 + 12, y * 16 + 8 - 24)
+        self.ttl = ttl
+        self.delay = delay
+
+    def update(self):
+        if self.delay:
+            self.delay -= 1
+            self.image.set_alpha(max(0, 255 - self.delay * 24))
+            return
+        self.rect.move_ip(0, -2)
+        self.ttl -= 1
+        if self.ttl < 5:
+            self.image.set_alpha(self.ttl * 51)
+        if self.ttl <= 0:
+            self.kill()
+
+class Caption(object):
+    def __init__(self, x, y, text, color=(255, 255, 255), ttl=16):
+        self.x = x
+        self.y = y
+        self.text = text
+        self.color = color
+        self.ttl = ttl
+
+    def render(self, to_display, delay=0):
+        self.sprite = CaptionSprite(self.x, self.y, self.text,
+                                    self.color, delay, self.ttl)
+        to_display.effects.add(self.sprite)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/generator.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,184 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import random
+import thing
+import monster
+
+
+class LevelGenerator(object):
+    depth_monsters = [
+        [monster.Bat],
+        [monster.Bat, monster.Skeleton],
+        [monster.Bat, monster.Skeleton, monster.Zombie],
+        [monster.Skeleton, monster.Zombie],
+        [monster.Skeleton, monster.Zombie, monster.Jelly],
+        [monster.Skeleton, monster.Zombie, monster.Jelly, monster.Ghost],
+        [monster.Jelly, monster.Zombie, monster.Ghost],
+        [monster.Jelly, monster.Ghost, monster.Jack],
+        [monster.Ghost, monster.Jack],
+        [monster.Ghost, monster.Jack, monster.Eye],
+        [monster.Jack, monster.Eye],
+        [monster.Bat, monster.Jack, monster.Eye],
+        [monster.Bat, monster.Skeleton, monster.Zombie, monster.Jack,
+         monster.Eye, monster.Jelly, monster.Ghost],
+    ]
+
+    def __init__(self, depth):
+        self.depth = depth
+        self.map = [['x'] * 30 for i in range(30)]
+        self.things = {}
+        self.monsters = {}
+        self.rooms = [[self.make_room(x, y) for x in range(3)]
+                      for y in range(3)]
+        for line in self.rooms:
+            for x1, y1, x2, y2 in line:
+                self.rect(x1, y1, x2, y2, 'X', '.+:')
+        self.make_connections()
+        for line in self.map:
+            for i, c in enumerate(line):
+                if c == 'X':
+                    line[i] = 'x'
+        self.make_monsters()
+        self.make_trash()
+        self.make_stairs()
+
+    def _random_place(self):
+        r1 = random.choice(random.choice(self.rooms))
+        x1 = random.randint(r1[0] + 1, r1[2] - 1)
+        y1 = random.randint(r1[1] + 1, r1[3] - 1)
+        return x1, y1
+
+    def make_trash(self):
+        trash = [thing.Plant, thing.Chest]
+        for i in range(random.randint(5, 15)):
+            p = self._random_place()
+            if p not in self.things:
+                self.things[p] = random.choice(trash)(*p)
+
+    def make_monsters(self):
+        for i in range(15):
+            p = self._random_place()
+            if (p not in self.monsters and
+                not isinstance(self.things.get(p), thing.Entrance)):
+                monsters = self.depth_monsters[self.depth]
+                self.monsters[p] = random.choice(monsters)(*p)
+
+    def make_stairs(self):
+        p = self._random_place()
+        while p in self.things:
+            p = self._random_place()
+        self.things[p] = thing.Entrance(*p)
+        p = self._random_place()
+        while p in self.things:
+            p = self._random_place()
+        x1, y1 = p
+        self.map[y1][x1] = '>'
+
+    def make_connections(self):
+        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
+        connections = []
+        connected = [(random.randint(0, 2), random.randint(0, 2))]
+        while len(connected) < 9:
+            x1, y1 = random.choice(connected)
+            try:
+                dx, dy = random.choice([(dx, dy) for (dx, dy)
+                    in dirs if 0 <= x1 + dx <= 2 and 0 <= y1 + dy <= 2 and
+                        (x1 + dx, y1 + dy) not in connected])
+            except IndexError:
+                # pick another room
+                continue
+            r1 = self.rooms[y1][x1]
+            x2, y2 = x1 + dx, y1 + dy
+            r2 = self.rooms[y2][x2]
+            self.connect_rooms(r1, r2)
+            connections.append((x1, y1, x2, y2))
+            connected.append((x2, y2))
+        for loop in range(3):
+            x1 = random.randint(0, 2)
+            y1 = random.randint(0, 2)
+            possible = [(dx, dy) for (dx, dy)
+                in dirs if 0 <= x1 + dx <= 2 and 0 <= y1 + dy <= 2 and
+                not ((x1, y1, x1 + dx, y1 + dy) in connections or
+                     (x1 + dx, y1 + dy, x1, y1) in connections)]
+            if not possible:
+                continue
+            dx, dy = random.choice(possible)
+            x2, y2 = x1 + dx, y1 + dy
+            r1 = self.rooms[y1][x1]
+            r2 = self.rooms[y2][x2]
+            self.connect_rooms(r1, r2)
+            connections.append((x1, y1, x2, y2))
+
+    def make_room(self, x1, y1):
+        w = random.randint(3, 8)
+        h = random.randint(3, 8)
+        x = 10 * x1 + random.randint(0, 8 - w)
+        y = 10 * y1 + random.randint(0, 8 - h)
+        return (x, y, x + w, y + h)
+
+    def connect_rooms(self, r1, r2):
+        if abs(r1[0] - r2[0]) < abs(r1[1] - r2[1]):
+            # vertical
+            v = True
+            if r1[1] > r2[1]:
+                r1, r2 = r2, r1
+            x1 = random.randint(r1[0] + 1, r1[2] - 1)
+            y1 = r1[3]
+            x2 = random.randint(r2[0] + 1, r2[2] - 1)
+            y2 = r2[1]
+            if y1 + 1 != y2 - 1:
+                m = random.randint(y1 + 1, y2 - 1)
+            else:
+                m = y1 + 1
+            for y in range(y1, m):
+                self.map[y][x1] = random.choice(',;')
+            if x1 < x2:
+                for x in range(x1, x2 + 1):
+                    self.map[m][x] = random.choice(';"')
+            else:
+                for x in range(x2, x1 + 1):
+                    self.map[m][x] = random.choice(';"')
+            for y in range(m, y2):
+                self.map[y][x2] = random.choice(',;')
+        else:
+            # horizontal
+            v = False
+            if r1[0] > r2[0]:
+                r1, r2 = r2, r1
+            x1 = r1[2]
+            y1 = random.randint(r1[1] + 1, r1[3] - 1)
+            x2 = r2[0]
+            y2 = random.randint(r2[1] + 1, r2[3] - 1)
+            if x1 + 1 != x2 - 1:
+                m = random.randint(x1 + 1, x2 - 1)
+            else:
+                m = x1 + 1
+            for x in range(x1, m):
+                self.map[y1][x] = random.choice(',;')
+            if y1 < y2:
+                for y in range(y1, y2 + 1):
+                    self.map[y][m] = random.choice(';"')
+            else:
+                for y in range(y2, y1 + 1):
+                    self.map[y][m] = random.choice(';"')
+            for x in range(m, x2):
+                self.map[y2][x] = random.choice(',;')
+        self.map[y1][x1] = '+'
+        self.map[y2][x2] = '+'
+        if random.randint(0, 10) > 7:
+            self.things[(x1, y1)] = thing.Door(x1, y1, not v)
+        if random.randint(0, 10) > 7:
+            self.things[(x2, y2)] = thing.Door(x2, y2, not v)
+
+    def rect(self, x1, y1, x2, y2, c, fill=None):
+        if fill:
+            for y in range(y1 + 1, y2):
+                for x in range(x1 + 1, x2):
+                    self.map[y][x] = random.choice(fill)
+        for y in range(y1, y2 + 1):
+            self.map[y][x1] = c
+            self.map[y][x2] = c
+        for x in range(x1, x2 + 1):
+            self.map[y1][x] = c
+            self.map[y2][x] = c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/hero.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,148 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import display
+import thing
+import effect
+
+
+class HeroSprite(display.Sprite):
+    faces = {
+        -1: { -1: 1, 0: 1, 1: 0 },
+         0: { -1: 2, 0: 0, 1: 0 },
+         1: { -1: 2, 0: 3, 1: 3},
+    }
+
+    def __init__(self):
+        super(HeroSprite, self).__init__(0, 0, 'hero.png')
+        self.facing = 2
+        self.animation = None
+
+    def update(self):
+        if self.animation:
+            try:
+                self.animation.next()
+            except StopIteration:
+                self.animation = None
+        else:
+            self.image = self.frames[self.facing][0]
+
+    def anim_walk(self, dx, dy):
+        self.facing = self.faces[dx][dy]
+        self.depth = self.rect.midbottom[1] + 1
+        for frame in range(4):
+            self.image = self.frames[self.facing][frame % 4]
+            for step in range(2):
+                yield
+                self.rect.move_ip(dx * 3, dy * 2)
+                self.depth = self.rect.midbottom[1] + 1
+
+    def anim_blocked(self, dx, dy):
+        self.facing = self.faces[dx][dy]
+        self.depth = self.rect.midbottom[1] + 1
+        for frame in range(4):
+            yield
+            self.image = self.frames[self.facing][frame % 4]
+            yield
+
+    def anim_enter(self):
+        self.depth = self.rect.midbottom[1] + 1
+        self.image = self.frames[2][0]
+        self.rect.move_ip(0, -15)
+        for step in range(6):
+            yield
+            self.rect.move_ip(0, step)
+
+    def anim_exit(self):
+        self.depth = self.rect.midbottom[1] + 1
+        image = self.frames[2][0].copy()
+        self.image = image
+        move = 0
+        for step in range(6):
+            yield
+            move += step
+            self.image = image.subsurface(0, 0, 32, 32 - move)
+            self.rect.move_ip(0, step)
+
+    def anim_fight(self, dx, dy):
+        self.facing = self.faces[dx][dy]
+        self.image = self.frames[self.facing][4]
+        h = [1, 2, -3]
+        for frame in range(3):
+            yield
+            self.rect.move_ip(dx * 3 * frame, dy * 2 * frame - h[frame])
+            self.depth = self.rect.midbottom[1] + 1 + h[frame]
+        self.depth = self.rect.midbottom[1] + 1
+        for frame in range(3):
+            yield
+            self.image = self.frames[self.facing][1]
+            self.rect.move_ip(-dx * 3 * frame, -dy * 2 * frame)
+            self.depth = self.rect.midbottom[1] + 1
+        self.image = self.frames[self.facing][0]
+        for frame in range(2):
+            yield
+
+
+class Hero(thing.Thing):
+    def __init__(self, x=0, y=0):
+        self.x, self.y = x, y
+        self.effects = []
+        self.sprite = HeroSprite()
+        self.max_hp = 10
+        self.hp = self.max_hp
+        self.regen = 10
+        self.regen_tick = 0
+
+    def render(self, to_display):
+        to_display.sprites.add(self.sprite)
+        to_display.sprites.add(thing.ShadowSprite(self.sprite))
+
+    def tick(self):
+        self.regen_tick += 1
+        if self.regen_tick > self.regen:
+            if self.hp < self.max_hp:
+                self.hp += 1
+                self.effects.append(
+                    effect.Caption(self.x, self.y, '+1', (32, 128, 0)))
+            self.regen_tick = 0
+
+    def enter(self, level, x, y):
+        self.x = x
+        self.y = y
+        self.sprite.move(x, y)
+        self.sprite.facing = 2
+        self.sprite.animation = self.sprite.anim_enter()
+
+    def exit(self, level):
+        self.sprite.animation = self.sprite.anim_exit()
+        level.hero = None
+
+    def walk(self, level, dx, dy):
+        block = level.is_blocked(self.x + dx, self.y + dy)
+        if not block:
+            self.x += dx
+            self.y += dy
+            self.sprite.animation = self.sprite.anim_walk(dx, dy)
+        elif hasattr(block, 'hit'):
+            self.attack(level, dx, dy, block)
+        else:
+            self.sprite.animation = self.sprite.anim_blocked(dx, dy)
+        self.sprite.animation.next()
+
+    def attack(self, level, dx, dy, what=None):
+        if what is None:
+            what = level.is_blocked(self.x + dx, self.y + dy)
+        if not hasattr(what, 'hit'):
+            return
+        what.hit()
+        self.sprite.animation = self.sprite.anim_fight(dx, dy)
+        if isinstance(what, thing.Chest):
+            self.effects.append(
+                effect.Caption(self.x + dx, self.y + dy, 'empty', (64, 192, 0)))
+
+    def hit(self, damage):
+        self.hp -= damage
+        self.regen_tick = 0
+        self.effects.append(
+            effect.Caption(self.x, self.y, '%d' % (-damage), (255, 32, 32)))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/level.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import display
+import thing
+import generator
+import pygame
+
+
+class Tile(object):
+    filename = 'level.png'
+    tx, ty = 0, 3
+    blocks = False
+
+    def __init__(self, x, y, map):
+        self.x = x
+        self.y = y
+        self.map = map
+
+    def render(self, to_display):
+        tile = display.tiles[self.filename][self.tx][self.ty]
+        to_display.background.blit(tile, (self.x * 24, self.y * 16))
+
+
+class BrokenTile(Tile):
+    tx, ty = 2, 3
+
+
+class GroundTile(Tile):
+    tx, ty = 0, 4
+
+
+class PlantsTile(Tile):
+    tx, ty = 1, 4
+
+
+class WeedTile(Tile):
+    tx, ty = 2, 4
+
+
+class GrassTile(Tile):
+    tx, ty = 3, 4
+
+
+class DoorTile(Tile):
+    tx, ty = 3, 3
+
+
+class StairsTile(Tile):
+    tx, ty = 1, 3
+
+
+class WallTile(Tile):
+    blocks = True
+
+    def render(self, to_display):
+        map = self.map
+        x = self.x
+        y = self.y
+        c = map[y][x]
+        tileset = display.tiles[self.filename]
+        def f(x, y):
+            return map[y][x] == c
+        def sides(x, y):
+            if y < 0:
+                return 3
+            if x <= 0 or f(x - 1, y):
+                if x >= len(map[y])-1 or f(x + 1, y):
+                    return 1
+                else:
+                    return 2
+            elif x >= len(map[y])-1 or f(x + 1, y):
+                return 0
+            return 3
+        if y > 0 and not f(x, y - 1):
+            overlay = display.Overlay(x, y - 1, tileset[sides(x, y)][0])
+            to_display.overlay.add(overlay)
+        if y < len(map) - 1:
+            if f(x, y + 1):
+                tile = tileset[sides(x, y + 1)][1]
+            else:
+                tile = tileset[sides(x, y)][2]
+            to_display.background.blit(tile, (self.x * 24, self.y * 16))
+
+
+class Level(object):
+    key = {
+        '.': Tile,
+        '+': DoorTile,
+        ':': BrokenTile,
+        '>': StairsTile,
+        'x': WallTile,
+        ',': GroundTile,
+        ';': PlantsTile,
+        '"': WeedTile,
+        '*': GrassTile,
+    }
+    BLOCKED = 1
+
+    def __init__(self, hero, depth):
+        self.hero = hero
+        self.depth = depth
+        gen = generator.LevelGenerator(depth)
+        self.map = gen.map
+        self.things = {}
+        for t in gen.things.itervalues():
+            self.things[(t.x, t.y)] = t
+            if isinstance(t, thing.Entrance):
+                self.hero.enter(self, t.x, t.y)
+        self.monsters = {}
+        for m in gen.monsters.itervalues():
+            self.monsters[(m.x, m.y)] = m
+
+    def is_blocked(self, x, y):
+        if (x, y) in self.monsters:
+            return self.monsters[(x, y)]
+        t = self.things.get((x, y))
+        if t and t.blocks:
+            return t
+        if x<0 or y<0:
+            return True
+        try:
+            return self.flags[y][x] & self.BLOCKED
+        except IndexError:
+            return True
+
+    def render(self, display, area=None):
+        if area is None:
+            area = (0, 0, len(self.map[0]), len(self.map))
+        display.background = pygame.Surface((len(self.map[0]) * 24,
+                                             len(self.map) * 16))
+        self.flags = []
+        for y, line in enumerate(self.map[area[1]:area[3]]):
+            self.flags.append([])
+            for x, c in enumerate(line[area[0]:area[2]]):
+                tile_class = self.key.get(c)
+                if tile_class:
+                    tile = tile_class(x, y, self.map)
+                    tile.render(display)
+                self.flags[-1].append(0)
+                if tile_class is None or tile.blocks:
+                    self.flags[-1][-1] |= self.BLOCKED
+        self.hero.render(display)
+        for t in self.things.itervalues():
+            t.render(display)
+        for m in self.monsters.itervalues():
+            m.render(display)
+
+    def render_map(self, display, area):
+        display.background.fill((0, 0, 0), rect=Rect(area[0]*24, area[1]*16,
+            (area[2] - area[0]) * 24, (area[3] - area[1]) * 16))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import pygame.locals
+
+import display
+import level
+import hero
+import menu
+
+class Game(object):
+    def __init__(self):
+        self.depth = 0
+        self.display = display.Display((320, 200))
+        self.hero = hero.Hero()
+        self.start_level()
+        self.health = menu.GameInfo(288, 8, 'HP %d/10' % self.hero.hp,
+                                    (32, 192, 0))
+        self.display.menus.add(self.health)
+
+    def start_level(self):
+        self.display.empty()
+        self.level = level.Level(self.hero, self.depth)
+        self.depth += 1
+        self.level.render(self.display)
+        self.display.scroll_to(self.hero.sprite.rect)
+
+    def control(self):
+        if self.display.key == pygame.locals.K_ESCAPE:
+            raise display.GameQuit()
+        end_turn = False
+        if not self.hero.sprite.animation:
+            if self.level.hero is None:
+                self.start_level()
+            if self.level.map[self.hero.y][self.hero.x] == '>':
+                self.hero.exit(self.level)
+                return
+            pressed = self.display.get_pressed()
+            if pygame.locals.K_UP in pressed:
+                self.hero.walk(self.level, 0, -1)
+                end_turn = True
+            elif pygame.locals.K_DOWN in pressed:
+                self.hero.walk(self.level, 0, 1)
+                end_turn = True
+            elif pygame.locals.K_LEFT in pressed:
+                self.hero.walk(self.level, -1, 0)
+                end_turn = True
+            elif pygame.locals.K_RIGHT in pressed:
+                self.hero.walk(self.level, 1, 0)
+                end_turn = True
+        if end_turn:
+            self.hero.tick()
+            self.health.change('HP %d/10' % self.hero.hp)
+            monsters = self.level.monsters.values()
+            for m in monsters:
+                if m.dead:
+                    del self.level.monsters[(m.x, m.y)]
+            for m in monsters:
+                if not m.dead:
+                    m.act(self.level)
+            for m in monsters + [self.hero]:
+                for d, effect in enumerate(m.effects):
+                    effect.render(self.display, delay=d * 12)
+                m.effects = []
+
+    def main(self):
+        try:
+            while True:
+                self.display.tick(tick=24)
+                self.control()
+                self.display.scroll_after(self.hero.sprite.rect)
+        except display.GameQuit:
+            pass
+
+if __name__ == '__main__':
+    Game().main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/menu.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,25 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import pygame
+import effect
+
+
+class GameInfo(pygame.sprite.Sprite):
+    def __init__(self, x, y, text, color):
+        super(GameInfo, self).__init__()
+        self.x, self.y = x, y
+        self.text = text
+        self.color = color
+        self.image = effect.outlined_text(text, color, (32, 32, 32))
+        self.rect = self.image.get_rect()
+        self.rect.center = self.x, self.y
+
+    def change(self, text=None, color=None):
+        if text is not None:
+            self.text = text
+        if color is not None:
+            self.color = color
+        self.image = effect.outlined_text(self.text, self.color, (32, 32, 32))
+        self.rect = self.image.get_rect()
+        self.rect.center = self.x, self.y
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/monster.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,233 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import random
+
+import thing
+import effect
+import display
+
+
+dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
+
+
+def distance(x1, y1, x2, y2):
+    return abs(x1 - x2) + abs(y1 - y2)
+
+
+class MonsterSprite(display.Sprite):
+    def __init__(self, x, y, tx):
+        self.tx = tx
+        self.ty = 0
+        super(MonsterSprite, self).__init__(x, y, 'monsters.png')
+        self.animation = self.anim_default()
+        self.frame = 0
+
+    def update(self):
+        try:
+            self.animation.next()
+        except StopIteration:
+            self.animation = self.anim_default()
+            self.animation.next()
+
+    def anim_default(self):
+        self.depth = self.rect.midbottom[1] + 1
+        while True:
+            yield
+            self.frame = (self.frame + 1) % 4
+            self.image = self.frames[self.tx][self.frame % 4]
+            yield
+            yield
+
+    def anim_walk(self, dx, dy):
+        for frame in range(2):
+            self.frame = (self.frame + 1) % 4
+            for step in range(4):
+                yield
+                self.image = self.frames[self.tx][self.frame]
+                self.rect.move_ip(dx * 3, dy * 2)
+                self.depth = self.rect.midbottom[1] + 1
+
+    def anim_fight(self, dx, dy):
+        self.image = self.frames[self.tx][1]
+        h = [1, 2, -3]
+        latitude = 0
+        for frame in range(2):
+            yield
+        for frame in range(3):
+            yield
+            self.rect.move_ip(dx * 3 * frame, dy * 2 * frame - h[frame])
+            latitude += h[frame]
+            self.depth = self.rect.midbottom[1] - latitude + 1
+        self.depth = self.rect.midbottom[1]
+        for frame in range(3):
+            yield
+            self.image = self.frames[self.tx][1]
+            self.rect.move_ip(-dx * 3 * frame, -dy * 2 * frame)
+            self.depth = self.rect.midbottom[1] + 1
+        self.image = self.frames[self.tx][0]
+
+    def anim_die(self):
+        image = self.frames[self.tx][0]
+        edge = 4
+        yield
+        yield
+        for frame in range(8):
+            yield
+            self.image = image.subsurface(0, 0, 32, 32 - edge)
+            edge += frame
+            self.image.set_alpha(256 - frame * 32)
+            self.rect.move_ip(0, frame - 1)
+        self.kill()
+
+
+class Monster(thing.Thing):
+    tx = 0
+    max_hp = 4
+    name = "monster"
+    alpha = None
+
+    def __init__(self, x, y):
+        self.x, self.y = x, y
+        self.dead = False
+        self.effects = []
+        self.hp = self.max_hp
+        self.asleep = False
+
+    def render(self, to_display):
+        self.sprite = MonsterSprite(self.x, self.y, self.tx)
+        if self.alpha:
+            for frame in self.sprite.frames[self.tx]:
+                frame.set_alpha(self.alpha)
+        to_display.sprites.add(self.sprite)
+        to_display.sprites.add(thing.ShadowSprite(self.sprite))
+
+    def hit(self, damage=1):
+        self.asleep = False
+        self.effects.append(
+            effect.Caption(self.x, self.y, '%d' % (-damage), (255, 128, 16)))
+        self.hp -= damage
+        if self.hp <= 0:
+            self.die()
+
+    def die(self):
+        c = effect.Caption(self.x, self.y, self.name, (192, 192, 192), ttl=8)
+        self.effects.append(c)
+        self.dead = True
+        self.sprite.animation = self.sprite.anim_die()
+        self.sprite.animation.next()
+
+    def act(self, level):
+        # Attack if standing next to the hero
+        if distance(level.hero.x, level.hero.y, self.x, self.y) == 1:
+            self.attack(level, level.hero.x - self.x, level.hero.y - self.y)
+            return
+
+        if self.asleep:
+            return
+
+        # Walk towards the hero
+        possible = [(dx, dy) for (dx, dy) in dirs if
+                    not self.is_blocked(level, dx, dy)]
+        if not possible:
+            return
+        random.shuffle(possible)
+        dx, dy = min(possible,
+            key=lambda d: distance(level.hero.x, level.hero.y,
+                                        self.x+d[0], self.y+d[1]))
+        self.walk(level, dx, dy)
+
+    def is_blocked(self, level, dx, dy):
+        block = level.is_blocked(self.x + dx, self.y + dy)
+        if block:
+            return True
+
+    def walk(self, level, dx, dy):
+        if self.is_blocked(level, dx, dy):
+            return
+        if (level.hero.x, level.hero.y) == (self.x + dx, self.y + dy):
+            self.attack(level, dx, dy)
+            return
+        del level.monsters[(self.x, self.y)]
+        self.x += dx
+        self.y += dy
+        level.monsters[(self.x, self.y)] = self
+        self.sprite.animation = self.sprite.anim_walk(dx, dy)
+        self.sprite.animation.next()
+
+    def attack(self, level, dx, dy):
+        self.asleep = False
+        if not (level.hero.x, level.hero.y) == (self.x + dx, self.y + dy):
+            return
+        self.sprite.animation = self.sprite.anim_fight(dx, dy)
+        self.sprite.animation.next()
+        level.hero.hit(1)
+
+
+class Skeleton(Monster):
+    tx = 0
+    name = "skeleton"
+    max_hp = 4
+
+
+class Zombie(Monster):
+    tx = 1
+    name = "zombie"
+    max_hp = 7
+
+
+class Jack(Monster):
+    tx = 2
+    name = "jack"
+    max_hp = 8
+
+
+class Bat(Monster):
+    tx = 3
+    name = "bat"
+    max_hp = 2
+
+    def act(self, level):
+        # Attack if we feel like it
+        if random.choice([True, False]):
+            if distance(level.hero.x, level.hero.y, self.x, self.y) == 1:
+                self.attack(level, level.hero.x - self.x, level.hero.y - self.y)
+                return
+
+        if self.asleep:
+            return
+
+        # Move randomly (very annoying)
+        possible = [(dx, dy) for (dx, dy) in dirs if
+                    not self.is_blocked(level, dx, dy)]
+        if not possible:
+            return
+        dx, dy = random.choice(possible)
+        self.walk(level, dx, dy)
+
+
+class Ghost(Monster):
+    tx = 4
+    name = "ghost"
+    max_hp = 5
+    alpha = 96
+
+    def is_blocked(self, level, dx, dy):
+        # Ghosts can pass through obstacles
+        block = level.is_blocked(self.x + dx, self.y + dy)
+        if block and isinstance(block, Monster) or block == True:
+            return True
+
+
+class Eye(Monster):
+    tx = 5
+    name = "eye"
+    max_hp = 7
+    asleep = True
+
+
+class Jelly(Monster):
+    tx = 6
+    name = "jelly"
+    max_hp = 6
+    alpha = 192
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/thing.py	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,86 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import display
+
+
+class ShadowSprite(display.Sprite):
+    def __init__(self, parent):
+        super(ShadowSprite, self).__init__(0, 0, 'things.png', 3, 3)
+        self.image.set_alpha(64)
+        self.parent = parent
+
+    def update(self):
+        if not self.parent.alive():
+            self.kill()
+            return
+        depth = getattr(self.parent, 'depth', None)
+        self.rect.midbottom = (self.parent.rect.midbottom[0], depth)
+
+class DoorSprite(display.Sprite):
+    def __init__(self, x, y, vertical):
+        if vertical:
+            tx = 1
+        else:
+            tx = 0
+        super(DoorSprite, self).__init__(x, y, 'things.png', tx, 0)
+        self.animation = None
+
+    def update(self):
+        if self.animation:
+            try:
+                self.animation.next()
+            except StopIteration:
+                self.animation = None
+
+    def anim_open(self):
+        self.depth += 2
+        for step in range(6):
+            yield
+            self.rect.move_ip(0, -2)
+
+
+
+class Thing(object):
+    tx, ty = 0, 1
+    blocks = False
+
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+        self.sprite = display.Sprite(self.x, self.y, 'things.png',
+                                     self.tx, self.ty)
+
+    def render(self, to_display):
+        to_display.sprites.add(self.sprite)
+        to_display.shadows.add(ShadowSprite(self.sprite))
+
+
+class Door(Thing):
+    def __init__(self, x, y, vertical):
+        self.x = x
+        self.y = y
+        self.blocks = True
+        self.vertical = vertical
+        self.sprite = DoorSprite(self.x, self.y, self.vertical)
+
+    def hit(self, damage=0):
+        self.sprite.animation = self.sprite.anim_open()
+        self.blocks = False
+
+
+class Entrance(Thing):
+    tx, ty = 3, 0
+
+
+class Chest(Thing):
+    tx, ty = 1, 1
+    blocks = True
+
+    def hit(self, damage=0):
+        self.sprite.image = self.sprite.frames[2][self.ty]
+        self.blocks = False
+
+
+class Plant(Thing):
+    tx, ty = 2, 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/start	Sat Aug 27 23:18:10 2011 +0200
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+base_dir = os.path.dirname(os.path.abspath(__file__))
+src_dir = os.path.join(base_dir, 'src')
+
+sys.path.insert(0, src_dir)
+
+import main
+
+main.Game().main()
--- a/thing.py	Wed Aug 24 21:24:01 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import display
-
-
-class ShadowSprite(display.Sprite):
-    def __init__(self, parent):
-        super(ShadowSprite, self).__init__(0, 0, 'things.png', 3, 3)
-        self.image.set_alpha(64)
-        self.parent = parent
-
-    def update(self):
-        if not self.parent.alive():
-            self.kill()
-            return
-        depth = getattr(self.parent, 'depth', None)
-        self.rect.midbottom = (self.parent.rect.midbottom[0], depth)
-
-class DoorSprite(display.Sprite):
-    def __init__(self, x, y, vertical):
-        if vertical:
-            tx = 1
-        else:
-            tx = 0
-        super(DoorSprite, self).__init__(x, y, 'things.png', tx, 0)
-        self.animation = None
-
-    def update(self):
-        if self.animation:
-            try:
-                self.animation.next()
-            except StopIteration:
-                self.animation = None
-
-    def anim_open(self):
-        self.depth += 2
-        for step in range(6):
-            yield
-            self.rect.move_ip(0, -2)
-
-
-
-class Thing(object):
-    tx, ty = 0, 1
-    blocks = False
-
-    def __init__(self, x, y):
-        self.x = x
-        self.y = y
-        self.sprite = display.Sprite(self.x, self.y, 'things.png',
-                                     self.tx, self.ty)
-
-    def render(self, to_display):
-        to_display.sprites.add(self.sprite)
-        to_display.shadows.add(ShadowSprite(self.sprite))
-
-
-class Door(Thing):
-    def __init__(self, x, y, vertical):
-        self.x = x
-        self.y = y
-        self.blocks = True
-        self.vertical = vertical
-        self.sprite = DoorSprite(self.x, self.y, self.vertical)
-
-    def hit(self, damage=0):
-        self.sprite.animation = self.sprite.anim_open()
-        self.blocks = False
-
-
-class Entrance(Thing):
-    tx, ty = 3, 0
-
-
-class Chest(Thing):
-    tx, ty = 1, 1
-    blocks = True
-
-    def hit(self, damage=0):
-        self.sprite.image = self.sprite.frames[2][self.ty]
-        self.blocks = False
-
-
-class Plant(Thing):
-    tx, ty = 2, 2
Binary file things.png has changed