changeset 548:ded4324b236e pyntnclick

Moved stuff around, broke everything.
author Jeremy Thurgood <firxen@gmail.com>
date Sat, 11 Feb 2012 13:10:18 +0200
parents 33ce7ff757c3
children 098ea4ea0d0d
files gamelib/constants.py gamelib/cursor.py gamelib/data.py gamelib/endscreen.py gamelib/gamescreen.py gamelib/main.py gamelib/menu.py gamelib/scenewidgets.py gamelib/sound.py gamelib/speech.py gamelib/state.py gamelib/widgets.py pyntnclick/constants.py pyntnclick/cursor.py pyntnclick/data.py pyntnclick/endscreen.py pyntnclick/gamescreen.py pyntnclick/main.py pyntnclick/menu.py pyntnclick/scenewidgets.py pyntnclick/sound.py pyntnclick/speech.py pyntnclick/state.py pyntnclick/version.py pyntnclick/widgets.py
diffstat 25 files changed, 1686 insertions(+), 1604 deletions(-) [+]
line wrap: on
line diff
--- a/gamelib/constants.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-# Useful constants
-# copyright boomslang team (see COPYRIGHT file for details)
-
-
-SCREEN = (800, 600)
-FREQ = 44100   # same as audio CD
-BITSIZE = -16  # unsigned 16 bit
-CHANNELS = 2   # 1 == mono, 2 == stereo
-BUFFER = 1024  # audio buffer size in no. of samples
-
-BUTTON_SIZE = 50
-SCENE_SIZE = (SCREEN[0], SCREEN[1] - BUTTON_SIZE)
-# Animation frame rate
-FRAME_RATE = 25
-
-DEBUG = False
-
-ENTER, LEAVE = 1, 2
--- a/gamelib/cursor.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-# cursor.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-# Sprite Cursor
-
-from albow.resource import get_image
-from albow.widget import Widget
-from pygame.sprite import Sprite, RenderUpdates
-from pygame.rect import Rect
-import pygame
-import pygame.color
-import pygame.cursors
-import pygame.mouse
-
-from gamelib.constants import SCENE_SIZE
-
-
-class CursorSprite(Sprite):
-    "A Sprite that follows the Cursor"
-
-    def __init__(self, filename, x=None, y=None):
-        Sprite.__init__(self)
-        self.filename = filename
-        self.pointer_x = x
-        self.pointer_y = y
-        self.highlighted = False
-
-    def load(self):
-        if not hasattr(self, 'plain_image'):
-            self.plain_image = get_image('items', self.filename)
-            self.image = self.plain_image
-            self.rect = self.image.get_rect()
-
-            if self.pointer_x is None:
-                self.pointer_x = self.rect.size[0] // 2
-            if self.pointer_y is None:
-                self.pointer_y = self.rect.size[1] // 2
-
-            self.highlight = pygame.Surface(self.rect.size)
-            color = pygame.color.Color(255, 100, 100, 0)
-            self.highlight.fill(color)
-
-    def update(self):
-        pos = pygame.mouse.get_pos()
-        self.rect.left = pos[0] - self.pointer_x
-        self.rect.top = pos[1] - self.pointer_y
-
-    def set_highlight(self, enable):
-        if enable != self.highlighted:
-            self.load()
-            self.highlighted = enable
-            self.image = self.plain_image.copy()
-            if enable:
-                self.image.blit(self.highlight, self.highlight.get_rect(),
-                                None, pygame.BLEND_MULT)
-
-
-HAND = CursorSprite('hand.png', 12, 0)
-
-
-class CursorWidget(Widget):
-    """Mix-in widget to ensure that mouse_move is propogated to parents"""
-
-    cursor = HAND
-    _cursor_group = RenderUpdates()
-    _loaded_cursor = None
-
-    def __init__(self, screen, *args, **kwargs):
-        Widget.__init__(self, *args, **kwargs)
-        self.screen = screen
-
-    def enter_screen(self):
-        pygame.mouse.set_visible(0)
-
-    def leave_screen(self):
-        pygame.mouse.set_visible(1)
-
-    def draw_all(self, surface):
-        Widget.draw_all(self, surface)
-        self.draw_cursor(self.get_root().surface)
-
-    def draw_cursor(self, surface):
-        self.set_cursor(self.screen.state.tool)
-        self.cursor.set_highlight(self.cursor_highlight())
-        if self.cursor is not None:
-            self._cursor_group.update()
-            self._cursor_group.draw(surface)
-
-    def mouse_delta(self, event):
-        self.invalidate()
-
-    @classmethod
-    def set_cursor(cls, item):
-        if item is None or item.CURSOR is None:
-            cls.cursor = HAND
-        else:
-            cls.cursor = item.CURSOR
-        if cls.cursor != cls._loaded_cursor:
-            cls._loaded_cursor = cls.cursor
-            if cls.cursor is None:
-                pygame.mouse.set_visible(1)
-                cls._cursor_group.empty()
-            else:
-                pygame.mouse.set_visible(0)
-                cls.cursor.load()
-                cls._cursor_group.empty()
-                cls._cursor_group.add(cls.cursor)
-
-    def cursor_highlight(self):
-        if not Rect((0, 0), SCENE_SIZE).collidepoint(pygame.mouse.get_pos()):
-            return False
-        if self.screen.state.highlight_override:
-            return True
-        current_thing = self.screen.state.current_thing
-        if current_thing:
-            return current_thing.is_interactive()
-        return False
--- a/gamelib/data.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-'''Simple data loader module.
-
-Loads data files from the "data" directory shipped with a game.
-
-Enhancing this to handle caching etc. is left as an exercise for the reader.
-'''
-
-import os
-
-data_py = os.path.abspath(os.path.dirname(__file__))
-data_dir = os.path.normpath(os.path.join(data_py, '..', 'Resources'))
-
-
-def filepath(filename):
-    '''Determine the path to a file in the data directory.
-    '''
-    return os.path.join(data_dir, filename)
-
-
-def load(filename, mode='rb'):
-    '''Open a file in the data directory.
-
-    "mode" is passed as the second arg to open().
-    '''
-    return open(os.path.join(data_dir, filename), mode)
--- a/gamelib/endscreen.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-# endscreen.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-# Victory screen for the game
-
-from albow.screen import Screen
-from albow.resource import get_image
-
-from gamelib.widgets import BoomImageButton
-
-
-class EndImageButton(BoomImageButton):
-
-    FOLDER = 'won'
-
-
-class EndScreen(Screen):
-    def __init__(self, shell):
-        Screen.__init__(self, shell)
-        self.background = get_image('won', 'won.png')
-        self._menu_button = EndImageButton('menu.png', 26, 500,
-                action=self.main_menu)
-        self._quit_button = EndImageButton('quit.png', 250, 500,
-                action=shell.quit)
-        self.add(self._menu_button)
-        self.add(self._quit_button)
-
-    def draw(self, surface):
-        surface.blit(self.background, (0, 0))
-        self._menu_button.draw(surface)
-        self._quit_button.draw(surface)
-
-    def main_menu(self):
-        self.shell.show_screen(self.shell.menu_screen)
--- a/gamelib/gamescreen.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,261 +0,0 @@
-# gamescreen.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-# Main menu for the game
-
-from albow.controls import Widget
-from albow.layout import Row
-from albow.palette_view import PaletteView
-from albow.screen import Screen
-from pygame import Rect, mouse
-from pygame.color import Color
-
-from constants import SCREEN, BUTTON_SIZE, SCENE_SIZE, LEAVE
-from cursor import CursorWidget
-from state import initial_state, handle_result
-from widgets import (MessageDialog, BoomButton, HandButton, PopupMenu,
-                     PopupMenuButton)
-
-
-class InventoryView(PaletteView):
-
-    sel_color = Color("yellow")
-    sel_width = 2
-
-    def __init__(self, screen):
-        PaletteView.__init__(self, (BUTTON_SIZE, BUTTON_SIZE), 1, 6,
-                             scrolling=True)
-        self.screen = screen
-        self.state = screen.state
-        self.state_widget = screen.state_widget
-
-    def num_items(self):
-        return len(self.state.inventory)
-
-    def draw_item(self, surface, item_no, rect):
-        item_image = self.state.inventory[item_no].get_inventory_image()
-        surface.blit(item_image, rect, None)
-
-    def click_item(self, item_no, event):
-        item = self.state.inventory[item_no]
-        if self.item_is_selected(item_no):
-            self.unselect()
-        elif item.is_interactive(self.state.tool):
-            result = item.interact(self.state.tool)
-            handle_result(result, self.state_widget)
-        else:
-            self.state.set_tool(self.state.inventory[item_no])
-
-    def mouse_down(self, event):
-        if event.button != 1:
-            self.state.cancel_doodah(self.screen)
-        else:
-            PaletteView.mouse_down(self, event)
-
-    def item_is_selected(self, item_no):
-        return self.state.tool is self.state.inventory[item_no]
-
-    def unselect(self):
-        self.state.set_tool(None)
-
-
-class StateWidget(Widget):
-
-    def __init__(self, screen):
-        Widget.__init__(self, Rect(0, 0, SCENE_SIZE[0], SCENE_SIZE[1]))
-        self.screen = screen
-        self.state = screen.state
-        self.detail = DetailWindow(screen)
-
-    def draw(self, surface):
-        if self.state.previous_scene and self.state.do_check == LEAVE:
-            # We still need to handle leave events, so still display the scene
-            self.state.previous_scene.draw(surface, self)
-        else:
-            self.state.current_scene.draw(surface, self)
-
-    def mouse_down(self, event):
-        self.mouse_move(event)
-        if event.button != 1:  # We have a right/middle click
-            self.state.cancel_doodah(self.screen)
-        elif self.subwidgets:
-            self.clear_detail()
-            self._mouse_move(event.pos)
-        else:
-            result = self.state.interact(event.pos)
-            handle_result(result, self)
-
-    def animate(self):
-        if self.state.animate():
-            # queue a redraw
-            self.invalidate()
-        # We do this here so we can get enter and leave events regardless
-        # of what happens
-        result = self.state.check_enter_leave(self.screen)
-        handle_result(result, self)
-
-    def mouse_move(self, event):
-        self.state.highlight_override = False
-        if not self.subwidgets:
-            self._mouse_move(event.pos)
-
-    def _mouse_move(self, pos):
-        self.state.highlight_override = False
-        self.state.current_scene.mouse_move(pos)
-        self.state.old_pos = pos
-
-    def show_message(self, message, style=None):
-        # Display the message as a modal dialog
-        MessageDialog(self.screen, message, 60, style=style).present()
-        # queue a redraw to show updated state
-        self.invalidate()
-        # The cursor could have gone anywhere
-        if self.subwidgets:
-            self.subwidgets[0]._mouse_move(mouse.get_pos())
-        else:
-            self._mouse_move(mouse.get_pos())
-
-    def show_detail(self, detail):
-        self.clear_detail()
-        detail_obj = self.state.set_current_detail(detail)
-        self.detail.set_image_rect(Rect((0, 0), detail_obj.get_detail_size()))
-        self.add_centered(self.detail)
-        self.state.do_enter_detail()
-
-    def clear_detail(self):
-        """Hide the detail view"""
-        if self.state.current_detail is not None:
-            self.remove(self.detail)
-            self.state.do_leave_detail()
-            self.state.set_current_detail(None)
-            self._mouse_move(mouse.get_pos())
-
-    def end_game(self):
-        self.screen.running = False
-        self.screen.shell.show_screen(self.screen.shell.end_screen)
-
-
-class DetailWindow(Widget):
-    def __init__(self, screen):
-        Widget.__init__(self)
-        self.image_rect = None
-        self.screen = screen
-        self.state = screen.state
-        self.border_width = 5
-        self.border_color = (0, 0, 0)
-        # parent only gets set when we get added to the scene
-        self.close = BoomButton('Close', self.close_but, screen)
-        self.add(self.close)
-
-    def close_but(self):
-        self.parent.clear_detail()
-
-    def end_game(self):
-        self.parent.end_game()
-
-    def set_image_rect(self, rect):
-        bw = self.border_width
-        self.image_rect = rect
-        self.image_rect.topleft = (bw, bw)
-        self.set_rect(rect.inflate(bw * 2, bw * 2))
-        self.close.rect.midbottom = rect.midbottom
-
-    def draw(self, surface):
-        scene_surface = self.get_root().surface.subsurface(self.parent.rect)
-        overlay = scene_surface.convert_alpha()
-        overlay.fill(Color(0, 0, 0, 191))
-        scene_surface.blit(overlay, (0, 0))
-        self.state.current_detail.draw(
-            surface.subsurface(self.image_rect), self)
-
-    def mouse_down(self, event):
-        self.mouse_move(event)
-        if event.button != 1:  # We have a right/middle click
-            self.state.cancel_doodah(self.screen)
-        else:
-            result = self.state.interact_detail(
-                self.global_to_local(event.pos))
-            handle_result(result, self)
-
-    def mouse_move(self, event):
-        self._mouse_move(event.pos)
-
-    def _mouse_move(self, pos):
-        self.state.highlight_override = False
-        self.state.current_detail.mouse_move(self.global_to_local(pos))
-
-    def show_message(self, message, style=None):
-        self.parent.show_message(message, style)
-        self.invalidate()
-
-
-class ToolBar(Row):
-    def __init__(self, items):
-        for item in items:
-            item.height = BUTTON_SIZE
-        Row.__init__(self, items, spacing=0, width=SCREEN[0])
-        self.bg_color = (31, 31, 31)
-
-    def draw(self, surface):
-        surface.fill(self.bg_color)
-        Row.draw(self, surface)
-
-
-class GameScreen(Screen, CursorWidget):
-    def __init__(self, shell):
-        CursorWidget.__init__(self, self)
-        Screen.__init__(self, shell)
-        self.running = False
-
-    def _clear_all(self):
-        for widget in self.subwidgets[:]:
-            self.remove(widget)
-
-    def start_game(self):
-        self._clear_all()
-        self.state = initial_state()
-        self.state_widget = StateWidget(self)
-        self.add(self.state_widget)
-
-        self.popup_menu = PopupMenu(self)
-        self.menubutton = PopupMenuButton('Menu',
-                action=self.popup_menu.show_menu)
-
-        self.handbutton = HandButton(action=self.hand_pressed)
-
-        self.inventory = InventoryView(self)
-
-        self.toolbar = ToolBar([
-                self.menubutton,
-                self.handbutton,
-                self.inventory,
-                ])
-        self.toolbar.bottomleft = self.bottomleft
-        self.add(self.toolbar)
-
-        self.running = True
-
-    def enter_screen(self):
-        CursorWidget.enter_screen(self)
-
-    def leave_screen(self):
-        CursorWidget.leave_screen(self)
-
-    # Albow uses magic method names (command + '_cmd'). Yay.
-    # Albow's search order means they need to be defined here, not in
-    # PopMenu, which is annoying.
-    def hide_cmd(self):
-        # This option does nothing, but the method needs to exist for albow
-        return
-
-    def main_menu_cmd(self):
-        self.shell.show_screen(self.shell.menu_screen)
-
-    def quit_cmd(self):
-        self.shell.quit()
-
-    def hand_pressed(self):
-        self.inventory.unselect()
-
-    def begin_frame(self):
-        if self.running:
-            self.state_widget.animate()
--- a/gamelib/main.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-'''Game main module.
-
-Contains the entry point used by the run_game.py script.
-
-'''
-
-# Albow looks for stuff in os.path[0], which isn't always where it expects.
-# The following horribleness fixes this.
-import sys
-import os.path
-right_path = os.path.dirname(os.path.dirname(__file__))
-sys.path.insert(0, right_path)
-from optparse import OptionParser
-
-import pygame
-from pygame.locals import SWSURFACE
-from albow.shell import Shell
-
-from menu import MenuScreen
-from gamescreen import GameScreen
-from endscreen import EndScreen
-from constants import (
-    SCREEN, FRAME_RATE, FREQ, BITSIZE, CHANNELS, BUFFER, DEBUG)
-from sound import no_sound, disable_sound
-import state
-import data
-
-
-def parse_args(args):
-    parser = OptionParser()
-    parser.add_option("--no-sound", action="store_false", default=True,
-            dest="sound", help="disable sound")
-    if DEBUG:
-        parser.add_option("--scene", type="str", default=None,
-            dest="scene", help="initial scene")
-        parser.add_option("--no-rects", action="store_false", default=True,
-            dest="rects", help="disable debugging rects")
-    opts, _ = parser.parse_args(args or [])
-    return opts
-
-
-class MainShell(Shell):
-    def __init__(self, display):
-        Shell.__init__(self, display)
-        self.menu_screen = MenuScreen(self)
-        self.game_screen = GameScreen(self)
-        self.end_screen = EndScreen(self)
-        self.set_timer(FRAME_RATE)
-        self.show_screen(self.menu_screen)
-
-
-def main():
-    opts = parse_args(sys.argv)
-    pygame.display.init()
-    pygame.font.init()
-    if opts.sound:
-        try:
-            pygame.mixer.init(FREQ, BITSIZE, CHANNELS, BUFFER)
-        except pygame.error, exc:
-            no_sound(exc)
-    else:
-        # Ensure get_sound returns nothing, so everything else just works
-        disable_sound()
-    if DEBUG:
-        if opts.scene is not None:
-            # debug the specified scene
-            state.DEBUG_SCENE = opts.scene
-        state.DEBUG_RECTS = opts.rects
-    display = pygame.display.set_mode(SCREEN, SWSURFACE)
-    pygame.display.set_icon(pygame.image.load(
-        data.filepath('icons/suspended_sentence24x24.png')))
-    pygame.display.set_caption("Suspended Sentence")
-    shell = MainShell(display)
-    try:
-        shell.run()
-    except KeyboardInterrupt:
-        pass
--- a/gamelib/menu.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-# menu.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-# Main menu for the game
-
-from albow.screen import Screen
-from albow.resource import get_image
-
-from gamelib.widgets import BoomImageButton
-
-
-class SplashButton(BoomImageButton):
-
-    FOLDER = 'splash'
-
-
-class MenuScreen(Screen):
-    def __init__(self, shell):
-        Screen.__init__(self, shell)
-        self._background = get_image('splash', 'splash.png')
-        self._start_button = SplashButton('play.png', 16, 523, self.start)
-        self._resume_button = SplashButton('resume.png', 256, 523, self.resume,
-                                           enable=self.check_running)
-        self._quit_button = SplashButton('quit.png', 580, 523, shell.quit)
-        self.add(self._start_button)
-        self.add(self._resume_button)
-        self.add(self._quit_button)
-
-    def draw(self, surface):
-        surface.blit(self._background, (0, 0))
-        self._start_button.draw(surface)
-        self._resume_button.draw(surface)
-        self._quit_button.draw(surface)
-
-    def start(self):
-        self.shell.game_screen.start_game()
-        self.shell.show_screen(self.shell.game_screen)
-
-    def check_running(self):
-        return self.shell.game_screen.running
-
-    def resume(self):
-        if self.shell.game_screen.running:
-            self.shell.show_screen(self.shell.game_screen)
--- a/gamelib/scenewidgets.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-"""Interactive elements within a Scene."""
-
-
-from pygame import Rect
-from pygame.color import Color
-from pygame.colordict import THECOLORS
-from pygame.surface import Surface
-from albow.resource import get_image
-
-from gamelib.state import Thing
-from gamelib.constants import DEBUG
-from gamelib.widgets import BoomLabel
-
-
-class Interact(object):
-
-    def __init__(self, image, rect, interact_rect):
-        self.image = image
-        self.rect = rect
-        self.interact_rect = interact_rect
-
-    def set_thing(self, thing):
-        pass
-
-    def draw(self, surface):
-        if self.image is not None:
-            surface.blit(self.image, self.rect, None)
-
-    def animate(self):
-        return False
-
-
-class InteractNoImage(Interact):
-
-    def __init__(self, x, y, w, h):
-        super(InteractNoImage, self).__init__(None, None, Rect(x, y, w, h))
-
-
-class InteractText(Interact):
-    """Display box with text to interact with -- mostly for debugging."""
-
-    def __init__(self, x, y, text, bg_color=None):
-        if bg_color is None:
-            bg_color = (127, 127, 127)
-        label = BoomLabel(text)
-        label.set_margin(5)
-        label.border_width = 1
-        label.border_color = (0, 0, 0)
-        label.bg_color = bg_color
-        label.fg_color = (0, 0, 0)
-        image = Surface(label.size)
-        rect = Rect((x, y), label.size)
-        label.draw_all(image)
-        super(InteractText, self).__init__(image, rect, rect)
-
-
-class InteractRectUnion(Interact):
-
-    def __init__(self, rect_list):
-        # pygame.rect.Rect.unionall should do this, but is broken
-        # in some pygame versions (including 1.8, it appears)
-        rect_list = [Rect(x) for x in rect_list]
-        union_rect = rect_list[0]
-        for rect in rect_list[1:]:
-            union_rect = union_rect.union(rect)
-        super(InteractRectUnion, self).__init__(None, None, union_rect)
-        self.interact_rect = rect_list
-
-
-class InteractImage(Interact):
-
-    def __init__(self, x, y, image_name):
-        super(InteractImage, self).__init__(None, None, None)
-        self._pos = (x, y)
-        self._image_name = image_name
-
-    def set_thing(self, thing):
-        self.image = get_image(thing.folder, self._image_name)
-        self.rect = Rect(self._pos, self.image.get_size())
-        self.interact_rect = self.rect
-
-
-class InteractImageRect(InteractImage):
-    def __init__(self, x, y, image_name, r_x, r_y, r_w, r_h):
-        super(InteractImageRect, self).__init__(x, y, image_name)
-        self._r_pos = (r_x, r_y)
-        self._r_size = (r_w, r_h)
-
-    def set_thing(self, thing):
-        super(InteractImageRect, self).set_thing(thing)
-        self.interact_rect = Rect(self._r_pos, self._r_size)
-
-
-class InteractAnimated(Interact):
-    """Interactive with an animation rather than an image"""
-
-    # FIXME: Assumes all images are the same size
-    # anim_seq - sequence of image names
-    # delay - number of frames to wait between changing images
-
-    def __init__(self, x, y, anim_seq, delay):
-        self._pos = (x, y)
-        self._anim_pos = 0
-        self._names = anim_seq
-        self._frame_count = 0
-        self._anim_seq = None
-        self._delay = delay
-
-    def set_thing(self, thing):
-        self._anim_seq = [get_image(thing.folder, x) for x in self._names]
-        self.image = self._anim_seq[0]
-        self.rect = Rect(self._pos, self.image.get_size())
-        self.interact_rect = self.rect
-
-    def animate(self):
-        if self._anim_seq:
-            self._frame_count += 1
-            if self._frame_count > self._delay:
-                self._frame_count = 0
-                self._anim_pos += 1
-                if self._anim_pos >= len(self._anim_seq):
-                    self._anim_pos = 0
-                self.image = self._anim_seq[self._anim_pos]
-                # queue redraw
-                return True
-        return False
-
-
-class GenericDescThing(Thing):
-    "Thing with an InteractiveUnionRect and a description"
-
-    INITIAL = "description"
-
-    def __init__(self, prefix, number, description, areas):
-        super(GenericDescThing, self).__init__()
-        self.description = description
-        self.name = '%s.%s' % (prefix, number)
-        self.interacts = {
-                'description': InteractRectUnion(areas)
-                }
-        if DEBUG:
-            # Individual colors to make debugging easier
-            self._interact_hilight_color = Color(THECOLORS.keys()[number])
-
-    def get_description(self):
-        return self.description
-
-    def is_interactive(self, tool=None):
-        return False
--- a/gamelib/sound.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-# Sound management for Suspended Sentence
-
-# This re-implements some of the albow.resource code to
-# a) work around an annoying bugs
-# b) add some missing functionality (disable_sound)
-
-import os
-
-import pygame
-from pygame.mixer import music
-from albow.resource import _resource_path, dummy_sound
-import albow.music
-
-sound_cache = {}
-
-
-def get_sound(*names):
-    if sound_cache is None:
-        return dummy_sound
-    path = _resource_path("sounds", names)
-    sound = sound_cache.get(path)
-    if not sound:
-        if not os.path.isfile(path):
-            missing_sound("File does not exist", path)
-            return dummy_sound
-        try:
-            from pygame.mixer import Sound
-        except ImportError, e:
-            no_sound(e)
-            return dummy_sound
-        try:
-            sound = Sound(path)
-        except pygame.error, e:
-            missing_sound(e, path)
-            return dummy_sound
-        sound_cache[path] = sound
-    return sound
-
-
-def no_sound(e):
-    global sound_cache
-    print "get_sound: %s" % e
-    print "get_sound: Sound not available, continuing without it"
-    sound_cache = None
-    albow.music.music_enabled = False
-
-
-def disable_sound():
-    global sound_cache
-    sound_cache = None
-    albow.music.music_enabled = False
-
-
-def missing_sound(e, name):
-    print "albow.resource.get_sound: %s: %s" % (name, e)
-
-
-def start_next_music():
-    """Start playing the next item from the current playlist immediately."""
-    if albow.music.music_enabled and albow.music.current_playlist:
-        next_music = albow.music.current_playlist.next()
-        if next_music:
-            #print "albow.music: loading", repr(next_music)
-            music.load(next_music)
-            music.play()
-            albow.music.next_change_delay = albow.music.change_delay
-        albow.music.current_music = next_music
-
-
-def get_current_playlist():
-    if albow.music.music_enabled and albow.music.current_playlist:
-        return albow.music.current_playlist
-
-# Monkey patch
-albow.music.start_next_music = start_next_music
--- a/gamelib/speech.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-# speech.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-# Speech playing and cache
-
-import re
-
-from sound import get_sound
-
-
-# cache of string -> sound object mappings
-_SPEECH_CACHE = {}
-
-# characters not to allow in filenames
-_REPLACE_RE = re.compile(r"[^a-z0-9-]+")
-
-
-class SpeechError(RuntimeError):
-    pass
-
-
-def get_filename(key, text):
-    """Simplify text to filename."""
-    filename = "%s-%s" % (key, text)
-    filename = filename.lower()
-    filename = _REPLACE_RE.sub("_", filename)
-    filename = filename[:30]
-    filename = "%s.ogg" % filename
-    return filename
-
-
-def get_speech(thing_name, text):
-    """Load a sound object from the cache."""
-    key = (thing_name, text)
-    if key in _SPEECH_CACHE:
-        return _SPEECH_CACHE[key]
-    filename = get_filename(thing_name, text)
-    _SPEECH_CACHE[key] = sound = get_sound("speech", filename)
-    return sound
-
-
-def say(thing_name, text):
-    """Play text as speech."""
-    sound = get_speech(thing_name, text)
-    sound.play()
--- a/gamelib/state.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,548 +0,0 @@
-"""Utilities and base classes for dealing with scenes."""
-
-import copy
-
-from albow.resource import get_image
-from albow.utils import frame_rect
-from widgets import BoomLabel
-from pygame.rect import Rect
-from pygame.color import Color
-
-import constants
-from scenes import SCENE_LIST, INITIAL_SCENE
-from sound import get_sound
-
-# override the initial scene to for debugging
-DEBUG_SCENE = None
-
-# whether to show debugging rects
-DEBUG_RECTS = False
-
-
-class Result(object):
-    """Result of interacting with a thing"""
-
-    def __init__(self, message=None, soundfile=None, detail_view=None,
-                 style=None, close_detail=False, end_game=False):
-        self.message = message
-        self.sound = None
-        if soundfile:
-            self.sound = get_sound(soundfile)
-        self.detail_view = detail_view
-        self.style = style
-        self.close_detail = close_detail
-        self.end_game = end_game
-
-    def process(self, scene_widget):
-        """Helper function to do the right thing with a result object"""
-        if self.sound:
-            self.sound.play()
-        if self.message:
-            scene_widget.show_message(self.message, self.style)
-        if self.detail_view:
-            scene_widget.show_detail(self.detail_view)
-        if (self.close_detail
-            and hasattr(scene_widget, 'parent')
-            and hasattr(scene_widget.parent, 'clear_detail')):
-            scene_widget.parent.clear_detail()
-        if self.end_game:
-            scene_widget.end_game()
-
-
-def handle_result(result, scene_widget):
-    """Handle dealing with result or result sequences"""
-    if result:
-        if hasattr(result, 'process'):
-            result.process(scene_widget)
-        else:
-            for res in result:
-                if res:
-                    # List may contain None's
-                    res.process(scene_widget)
-
-
-def initial_state():
-    """Load the initial state."""
-    state = GameState()
-    for scene in SCENE_LIST:
-        state.load_scenes(scene)
-    initial_scene = INITIAL_SCENE if DEBUG_SCENE is None else DEBUG_SCENE
-    state.set_current_scene(initial_scene)
-    state.set_do_enter_leave()
-    return state
-
-
-class GameState(object):
-    """Complete game state.
-
-    Game state consists of:
-
-    * items
-    * scenes
-    """
-
-    def __init__(self):
-        # map of scene name -> Scene object
-        self.scenes = {}
-        # map of detail view name -> DetailView object
-        self.detail_views = {}
-        # map of item name -> Item object
-        self.items = {}
-        # list of item objects in inventory
-        self.inventory = []
-        # currently selected tool (item)
-        self.tool = None
-        # current scene
-        self.current_scene = None
-        # current detail view
-        self.current_detail = None
-        # scene we came from, for enter and leave processing
-        self.previous_scene = None
-        # scene transion helpers
-        self.do_check = None
-        self.old_pos = None
-        # current thing
-        self.current_thing = None
-        self.highlight_override = False
-
-    def add_scene(self, scene):
-        self.scenes[scene.name] = scene
-
-    def add_detail_view(self, detail_view):
-        self.detail_views[detail_view.name] = detail_view
-
-    def add_item(self, item):
-        self.items[item.name] = item
-        item.set_state(self)
-
-    def load_scenes(self, modname):
-        mod = __import__("gamelib.scenes.%s" % (modname,), fromlist=[modname])
-        for scene_cls in mod.SCENES:
-            self.add_scene(scene_cls(self))
-        if hasattr(mod, 'DETAIL_VIEWS'):
-            for scene_cls in mod.DETAIL_VIEWS:
-                self.add_detail_view(scene_cls(self))
-
-    def set_current_scene(self, name):
-        old_scene = self.current_scene
-        self.current_scene = self.scenes[name]
-        self.current_thing = None
-        if old_scene and old_scene != self.current_scene:
-            self.previous_scene = old_scene
-            self.set_do_enter_leave()
-
-    def set_current_detail(self, name):
-        self.current_thing = None
-        if name is None:
-            self.current_detail = None
-        else:
-            self.current_detail = self.detail_views[name]
-            return self.current_detail
-
-    def add_inventory_item(self, name):
-        self.inventory.append(self.items[name])
-
-    def is_in_inventory(self, name):
-        if name in self.items:
-            return self.items[name] in self.inventory
-        return False
-
-    def remove_inventory_item(self, name):
-        self.inventory.remove(self.items[name])
-        # Unselect tool if it's removed
-        if self.tool == self.items[name]:
-            self.set_tool(None)
-
-    def replace_inventory_item(self, old_item_name, new_item_name):
-        """Try to replace an item in the inventory with a new one"""
-        try:
-            index = self.inventory.index(self.items[old_item_name])
-            self.inventory[index] = self.items[new_item_name]
-            if self.tool == self.items[old_item_name]:
-                self.set_tool(self.items[new_item_name])
-        except ValueError:
-            return False
-        return True
-
-    def set_tool(self, item):
-        self.tool = item
-
-    def interact(self, pos):
-        return self.current_scene.interact(self.tool, pos)
-
-    def interact_detail(self, pos):
-        return self.current_detail.interact(self.tool, pos)
-
-    def cancel_doodah(self, screen):
-        if self.tool:
-            self.set_tool(None)
-        elif self.current_detail:
-            screen.state_widget.clear_detail()
-
-    def do_enter_detail(self):
-        if self.current_detail:
-            self.current_detail.enter()
-
-    def do_leave_detail(self):
-        if self.current_detail:
-            self.current_detail.leave()
-
-    def animate(self):
-        if not self.do_check:
-            return self.current_scene.animate()
-
-    def check_enter_leave(self, screen):
-        if not self.do_check:
-            return None
-        if self.do_check == constants.LEAVE:
-            self.do_check = constants.ENTER
-            if self.previous_scene:
-                return self.previous_scene.leave()
-            return None
-        elif self.do_check == constants.ENTER:
-            self.do_check = None
-            # Fix descriptions, etc.
-            if self.old_pos:
-                self.current_scene.update_current_thing(self.old_pos)
-            return self.current_scene.enter()
-        raise RuntimeError('invalid do_check value %s' % self.do_check)
-
-    def set_do_enter_leave(self):
-        """Flag that we need to run the enter loop"""
-        self.do_check = constants.LEAVE
-
-
-class StatefulGizmo(object):
-
-    # initial data (optional, defaults to none)
-    INITIAL_DATA = None
-
-    def __init__(self):
-        self.data = {}
-        if self.INITIAL_DATA:
-            # deep copy of INITIAL_DATA allows lists, sets and
-            # other mutable types to safely be used in INITIAL_DATA
-            self.data.update(copy.deepcopy(self.INITIAL_DATA))
-
-    def set_data(self, key, value):
-        self.data[key] = value
-
-    def get_data(self, key):
-        return self.data.get(key, None)
-
-
-class Scene(StatefulGizmo):
-    """Base class for scenes."""
-
-    # sub-folder to look for resources in
-    FOLDER = None
-
-    # name of background image resource
-    BACKGROUND = None
-
-    # name of scene (optional, defaults to folder)
-    NAME = None
-
-    # Offset of the background image
-    OFFSET = (0, 0)
-
-    def __init__(self, state):
-        StatefulGizmo.__init__(self)
-        # scene name
-        self.name = self.NAME if self.NAME is not None else self.FOLDER
-        # link back to state object
-        self.state = state
-        # map of thing names -> Thing objects
-        self.things = {}
-        self._background = None
-
-    def add_item(self, item):
-        self.state.add_item(item)
-
-    def add_thing(self, thing):
-        self.things[thing.name] = thing
-        thing.set_scene(self)
-
-    def remove_thing(self, thing):
-        del self.things[thing.name]
-        if thing is self.state.current_thing:
-            self.state.current_thing.leave()
-            self.state.current_thing = None
-
-    def _get_description(self):
-        text = (self.state.current_thing and
-                self.state.current_thing.get_description())
-        if text is None:
-            return None
-        label = BoomLabel(text)
-        label.set_margin(5)
-        label.border_width = 1
-        label.border_color = (0, 0, 0)
-        label.bg_color = Color(210, 210, 210, 255)
-        label.fg_color = (0, 0, 0)
-        return label
-
-    def draw_description(self, surface, screen):
-        description = self._get_description()
-        if description is not None:
-            w, h = description.size
-            sub = screen.get_root().surface.subsurface(
-                Rect(400 - w / 2, 5, w, h))
-            description.draw_all(sub)
-
-    def _cache_background(self):
-        if self.BACKGROUND and not self._background:
-            self._background = get_image(self.FOLDER, self.BACKGROUND)
-
-    def draw_background(self, surface):
-        self._cache_background()
-        if self._background is not None:
-            surface.blit(self._background, self.OFFSET, None)
-        else:
-            surface.fill((200, 200, 200))
-
-    def draw_things(self, surface):
-        for thing in self.things.itervalues():
-            thing.draw(surface)
-
-    def draw(self, surface, screen):
-        self.draw_background(surface)
-        self.draw_things(surface)
-        self.draw_description(surface, screen)
-
-    def interact(self, item, pos):
-        """Interact with a particular position.
-
-        Item may be an item in the list of items or None for the hand.
-
-        Returns a Result object to provide feedback to the player.
-        """
-        if self.state.current_thing is not None:
-            return self.state.current_thing.interact(item)
-
-    def animate(self):
-        """Animate all the things in the scene.
-
-           Return true if any of them need to queue a redraw"""
-        result = False
-        for thing in self.things.itervalues():
-            if thing.animate():
-                result = True
-        return result
-
-    def enter(self):
-        return None
-
-    def leave(self):
-        return None
-
-    def update_current_thing(self, pos):
-        if self.state.current_thing is not None:
-            if not self.state.current_thing.contains(pos):
-                self.state.current_thing.leave()
-                self.state.current_thing = None
-        for thing in self.things.itervalues():
-            if thing.contains(pos):
-                thing.enter(self.state.tool)
-                self.state.current_thing = thing
-                break
-
-    def mouse_move(self, pos):
-        """Call to check whether the cursor has entered / exited a thing.
-
-        Item may be an item in the list of items or None for the hand.
-        """
-        self.update_current_thing(pos)
-
-    def get_detail_size(self):
-        self._cache_background()
-        return self._background.get_size()
-
-
-class InteractiveMixin(object):
-    def is_interactive(self, tool=None):
-        return True
-
-    def interact(self, tool):
-        if not self.is_interactive(tool):
-            return None
-        if tool is None:
-            return self.interact_without()
-        handler = getattr(self, 'interact_with_' + tool.tool_name, None)
-        inverse_handler = self.get_inverse_interact(tool)
-        if handler is not None:
-            return handler(tool)
-        elif inverse_handler is not None:
-            return inverse_handler(self)
-        else:
-            return self.interact_default(tool)
-
-    def get_inverse_interact(self, tool):
-        return None
-
-    def interact_without(self):
-        return self.interact_default(None)
-
-    def interact_default(self, item=None):
-        return None
-
-
-class Thing(StatefulGizmo, InteractiveMixin):
-    """Base class for things in a scene that you can interact with."""
-
-    # name of thing
-    NAME = None
-
-    # sub-folder to look for resources in (defaults to scenes folder)
-    FOLDER = None
-
-    # list of Interact objects
-    INTERACTS = {}
-
-    # name first interact
-    INITIAL = None
-
-    # Interact rectangle hi-light color (for debugging)
-    # (set to None to turn off)
-    _interact_hilight_color = Color('red')
-
-    def __init__(self):
-        StatefulGizmo.__init__(self)
-        # name of the thing
-        self.name = self.NAME
-        # folder for resource (None is overridden by scene folder)
-        self.folder = self.FOLDER
-        # interacts
-        self.interacts = self.INTERACTS
-        # these are set by set_scene
-        self.scene = None
-        self.state = None
-        self.current_interact = None
-        self.rect = None
-        self.orig_rect = None
-
-    def _fix_rect(self):
-        """Fix rects to compensate for scene offset"""
-        # Offset logic is to always work with copies, to avoid
-        # flying effects from multiple calls to _fix_rect
-        # See footwork in draw
-        if hasattr(self.rect, 'collidepoint'):
-            self.rect = self.rect.move(self.scene.OFFSET)
-        else:
-            self.rect = [x.move(self.scene.OFFSET) for x in self.rect]
-
-    def set_scene(self, scene):
-        assert self.scene is None
-        self.scene = scene
-        if self.folder is None:
-            self.folder = scene.FOLDER
-        self.state = scene.state
-        for interact in self.interacts.itervalues():
-            interact.set_thing(self)
-        self.set_interact(self.INITIAL)
-
-    def set_interact(self, name):
-        self.current_interact = self.interacts[name]
-        self.rect = self.current_interact.interact_rect
-        if self.scene:
-            self._fix_rect()
-        assert self.rect is not None, name
-
-    def contains(self, pos):
-        if hasattr(self.rect, 'collidepoint'):
-            return self.rect.collidepoint(pos)
-        else:
-            # FIXME: add sanity check
-            for rect in list(self.rect):
-                if rect.collidepoint(pos):
-                    return True
-        return False
-
-    def get_description(self):
-        return None
-
-    def enter(self, item):
-        """Called when the cursor enters the Thing."""
-        pass
-
-    def leave(self):
-        """Called when the cursr leaves the Thing."""
-        pass
-
-    def animate(self):
-        return self.current_interact.animate()
-
-    def draw(self, surface):
-        old_rect = self.current_interact.rect
-        if old_rect:
-            self.current_interact.rect = old_rect.move(self.scene.OFFSET)
-        self.current_interact.draw(surface)
-        self.current_interact.rect = old_rect
-        if DEBUG_RECTS and self._interact_hilight_color:
-            if hasattr(self.rect, 'collidepoint'):
-                frame_rect(surface, self._interact_hilight_color,
-                        self.rect.inflate(1, 1), 1)
-            else:
-                for rect in self.rect:
-                    frame_rect(surface, self._interact_hilight_color,
-                            rect.inflate(1, 1), 1)
-
-
-class Item(InteractiveMixin):
-    """Base class for inventory items."""
-
-    # image for inventory
-    INVENTORY_IMAGE = None
-
-    # name of item
-    NAME = None
-
-    # name for interactions (i.e. def interact_with_<TOOL_NAME>)
-    TOOL_NAME = None
-
-    # set to instance of CursorSprite
-    CURSOR = None
-
-    def __init__(self, name=None):
-        self.state = None
-        self.name = self.NAME
-        if name is not None:
-            self.name = name
-        self.tool_name = name
-        if self.TOOL_NAME is not None:
-            self.tool_name = self.TOOL_NAME
-        self.inventory_image = None
-
-    def _cache_inventory_image(self):
-        if not self.inventory_image:
-            self.inventory_image = get_image('items', self.INVENTORY_IMAGE)
-
-    def set_state(self, state):
-        assert self.state is None
-        self.state = state
-
-    def get_inventory_image(self):
-        self._cache_inventory_image()
-        return self.inventory_image
-
-    def get_inverse_interact(self, tool):
-        return getattr(tool, 'interact_with_' + self.tool_name, None)
-
-    def is_interactive(self, tool=None):
-        if tool:
-            return True
-        return False
-
-
-class CloneableItem(Item):
-    _counter = 0
-
-    @classmethod
-    def _get_new_id(cls):
-        cls._counter += 1
-        return cls._counter - 1
-
-    def __init__(self, name=None):
-        super(CloneableItem, self).__init__(name)
-        my_count = self._get_new_id()
-        self.name = "%s.%s" % (self.name, my_count)
--- a/gamelib/widgets.py	Sat Feb 11 12:52:29 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-# widgets.py
-# Copyright Boomslang team, 2010 (see COPYING File)
-
-"""Custom Albow widgets"""
-
-import textwrap
-
-import albow.controls
-import albow.menu
-from albow.resource import get_font, get_image
-from pygame.color import Color
-from pygame.rect import Rect
-from pygame.draw import lines as draw_lines
-from pygame import mouse
-
-from constants import BUTTON_SIZE
-from cursor import CursorWidget
-
-
-class BoomLabel(albow.controls.Label):
-
-    trim_line_top = 0
-
-    def __init__(self, text, width=None, **kwds):
-        albow.controls.Label.__init__(self, text, width, **kwds)
-        w, h = self.size
-        h -= self.trim_line_top * len(self.text.split('\n'))
-        self.size = (w, h)
-
-    def set_margin(self, margin):
-        """Add a set_margin method that recalculates the label size"""
-        old_margin = self.margin
-        w, h = self.size
-        d = margin - old_margin
-        self.margin = margin
-        self.size = (w + 2 * d, h + 2 * d)
-
-    def draw_all(self, surface):
-        bg_color = self.bg_color
-        self.bg_color = None
-        if bg_color is not None:
-            new_surface = surface.convert_alpha()
-            new_surface.fill(bg_color)
-            surface.blit(new_surface, surface.get_rect())
-        albow.controls.Label.draw_all(self, surface)
-        self._draw_all_no_bg(surface)
-        self.bg_color = bg_color
-
-    def _draw_all_no_bg(self, surface):
-        pass
-
-    def draw_with(self, surface, fg, _bg=None):
-        m = self.margin
-        align = self.align
-        width = surface.get_width()
-        y = m
-        lines = self.text.split("\n")
-        font = self.font
-        dy = font.get_linesize() - self.trim_line_top
-        for line in lines:
-            image = font.render(line, True, fg)
-            r = image.get_rect()
-            image = image.subsurface(r.clip(r.move(0, self.trim_line_top)))
-            r.top = y
-            if align == 'l':
-                r.left = m
-            elif align == 'r':
-                r.right = width - m
-            else:
-                r.centerx = width // 2
-            surface.blit(image, r)
-            y += dy
-
-
-class BoomButton(BoomLabel):
-
-    def __init__(self, text, action, screen):
-        super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'),
-                margin=4)
-        self.bg_color = (0, 0, 0)
-        self._frame_color = Color(50, 50, 50)
-        self.action = action
-        self.screen = screen
-
-    def mouse_down(self, event):
-        self.action()
-        self.screen.state_widget.mouse_move(event)
-
-    def mouse_move(self, event):
-        self.screen.state.highlight_override = True
-
-    def draw(self, surface):
-        super(BoomButton, self).draw(surface)
-        r = surface.get_rect()
-        w = 2
-        top, bottom, left, right = r.top, r.bottom, r.left, r.right
-        draw_lines(surface, self._frame_color, False, [
-            (left, bottom), (left, top), (right - w, top), (right - w, bottom)
-            ], w)
-
-
-class MessageDialog(BoomLabel, CursorWidget):
-
-    def __init__(self, screen, text, wrap_width, style=None, **kwds):
-        CursorWidget.__init__(self, screen)
-        self.set_style(style)
-        paras = text.split("\n\n")
-        text = "\n".join([textwrap.fill(para, wrap_width) for para in paras])
-        BoomLabel.__init__(self, text, **kwds)
-
-    def set_style(self, style):
-        self.set_margin(5)
-        self.border_width = 1
-        self.border_color = (0, 0, 0)
-        self.bg_color = (127, 127, 127)
-        self.fg_color = (0, 0, 0)
-        if style == "JIM":
-            self.set(font=get_font(20, "Monospace.ttf"))
-            self.trim_line_top = 10
-            self.bg_color = Color(255, 175, 127, 191)
-            self.fg_color = (0, 0, 0)
-            self.border_color = (127, 15, 0)
-
-    def draw_all(self, surface):
-        root_surface = self.get_root().surface
-        overlay = root_surface.convert_alpha()
-        overlay.fill(Color(0, 0, 0, 191))
-        root_surface.blit(overlay, (0, 0))
-        BoomLabel.draw_all(self, surface)
-
-    def _draw_all_no_bg(self, surface):
-        CursorWidget.draw_all(self, surface)
-
-    def mouse_down(self, event):
-        self.dismiss()
-        self.screen.state_widget._mouse_move(mouse.get_pos())
-        for widget in self.screen.state_widget.subwidgets:
-            widget._mouse_move(mouse.get_pos())
-
-    def cursor_highlight(self):
-        return False
-
-
-class HandButton(albow.controls.Image):
-    """The fancy hand button for the widget"""
-
-    def __init__(self, action):
-        # FIXME: Yes, please.
-        this_image = get_image('items', 'hand.png')
-        albow.controls.Image.__init__(self, image=this_image)
-        self.action = action
-        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
-
-    def mouse_down(self, event):
-        self.action()
-
-
-class PopupMenuButton(albow.controls.Button):
-
-    def __init__(self, text, action):
-        albow.controls.Button.__init__(self, text, action)
-
-        self.font = get_font(16, 'Vera.ttf')
-        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
-        self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2
-
-
-class PopupMenu(albow.menu.Menu, CursorWidget):
-
-    def __init__(self, screen):
-        CursorWidget.__init__(self, screen)
-        self.screen = screen
-        self.shell = screen.shell
-        items = [
-                ('Quit Game', 'quit'),
-                ('Exit to Main Menu', 'main_menu'),
-                ]
-        # albow.menu.Menu ignores title string
-        albow.menu.Menu.__init__(self, None, items)
-        self.font = get_font(16, 'Vera.ttf')
-
-    def show_menu(self):
-        """Call present, with the correct position"""
-        item_height = self.font.get_linesize()
-        menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE)
-        item = self.present(self.shell, (0, menu_top))
-        if item > -1:
-            # A menu item needs to be invoked
-            self.invoke_item(item)
-
-
-class BoomImageButton(albow.controls.Image):
-    """The fancy image button for the screens"""
-
-    FOLDER = None
-
-    def __init__(self, filename, x, y, action, enable=None):
-        this_image = get_image(self.FOLDER, filename)
-        albow.controls.Image.__init__(self, image=this_image)
-        self.action = action
-        self.set_rect(Rect((x, y), this_image.get_size()))
-        self.enable = enable
-
-    def draw(self, surface):
-        if self.is_enabled():
-            surface.blit(self.get_image(), self.get_rect())
-
-    def mouse_down(self, event):
-        if self.is_enabled():
-            self.action()
-
-    def is_enabled(self):
-        if self.enable:
-            return self.enable()
-        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/constants.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,18 @@
+# Useful constants
+# copyright boomslang team (see COPYRIGHT file for details)
+
+
+SCREEN = (800, 600)
+FREQ = 44100   # same as audio CD
+BITSIZE = -16  # unsigned 16 bit
+CHANNELS = 2   # 1 == mono, 2 == stereo
+BUFFER = 1024  # audio buffer size in no. of samples
+
+BUTTON_SIZE = 50
+SCENE_SIZE = (SCREEN[0], SCREEN[1] - BUTTON_SIZE)
+# Animation frame rate
+FRAME_RATE = 25
+
+DEBUG = False
+
+ENTER, LEAVE = 1, 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/cursor.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,116 @@
+# cursor.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+# Sprite Cursor
+
+from albow.resource import get_image
+from albow.widget import Widget
+from pygame.sprite import Sprite, RenderUpdates
+from pygame.rect import Rect
+import pygame
+import pygame.color
+import pygame.cursors
+import pygame.mouse
+
+from gamelib.constants import SCENE_SIZE
+
+
+class CursorSprite(Sprite):
+    "A Sprite that follows the Cursor"
+
+    def __init__(self, filename, x=None, y=None):
+        Sprite.__init__(self)
+        self.filename = filename
+        self.pointer_x = x
+        self.pointer_y = y
+        self.highlighted = False
+
+    def load(self):
+        if not hasattr(self, 'plain_image'):
+            self.plain_image = get_image('items', self.filename)
+            self.image = self.plain_image
+            self.rect = self.image.get_rect()
+
+            if self.pointer_x is None:
+                self.pointer_x = self.rect.size[0] // 2
+            if self.pointer_y is None:
+                self.pointer_y = self.rect.size[1] // 2
+
+            self.highlight = pygame.Surface(self.rect.size)
+            color = pygame.color.Color(255, 100, 100, 0)
+            self.highlight.fill(color)
+
+    def update(self):
+        pos = pygame.mouse.get_pos()
+        self.rect.left = pos[0] - self.pointer_x
+        self.rect.top = pos[1] - self.pointer_y
+
+    def set_highlight(self, enable):
+        if enable != self.highlighted:
+            self.load()
+            self.highlighted = enable
+            self.image = self.plain_image.copy()
+            if enable:
+                self.image.blit(self.highlight, self.highlight.get_rect(),
+                                None, pygame.BLEND_MULT)
+
+
+HAND = CursorSprite('hand.png', 12, 0)
+
+
+class CursorWidget(Widget):
+    """Mix-in widget to ensure that mouse_move is propogated to parents"""
+
+    cursor = HAND
+    _cursor_group = RenderUpdates()
+    _loaded_cursor = None
+
+    def __init__(self, screen, *args, **kwargs):
+        Widget.__init__(self, *args, **kwargs)
+        self.screen = screen
+
+    def enter_screen(self):
+        pygame.mouse.set_visible(0)
+
+    def leave_screen(self):
+        pygame.mouse.set_visible(1)
+
+    def draw_all(self, surface):
+        Widget.draw_all(self, surface)
+        self.draw_cursor(self.get_root().surface)
+
+    def draw_cursor(self, surface):
+        self.set_cursor(self.screen.state.tool)
+        self.cursor.set_highlight(self.cursor_highlight())
+        if self.cursor is not None:
+            self._cursor_group.update()
+            self._cursor_group.draw(surface)
+
+    def mouse_delta(self, event):
+        self.invalidate()
+
+    @classmethod
+    def set_cursor(cls, item):
+        if item is None or item.CURSOR is None:
+            cls.cursor = HAND
+        else:
+            cls.cursor = item.CURSOR
+        if cls.cursor != cls._loaded_cursor:
+            cls._loaded_cursor = cls.cursor
+            if cls.cursor is None:
+                pygame.mouse.set_visible(1)
+                cls._cursor_group.empty()
+            else:
+                pygame.mouse.set_visible(0)
+                cls.cursor.load()
+                cls._cursor_group.empty()
+                cls._cursor_group.add(cls.cursor)
+
+    def cursor_highlight(self):
+        if not Rect((0, 0), SCENE_SIZE).collidepoint(pygame.mouse.get_pos()):
+            return False
+        if self.screen.state.highlight_override:
+            return True
+        current_thing = self.screen.state.current_thing
+        if current_thing:
+            return current_thing.is_interactive()
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/data.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,25 @@
+'''Simple data loader module.
+
+Loads data files from the "data" directory shipped with a game.
+
+Enhancing this to handle caching etc. is left as an exercise for the reader.
+'''
+
+import os
+
+data_py = os.path.abspath(os.path.dirname(__file__))
+data_dir = os.path.normpath(os.path.join(data_py, '..', 'Resources'))
+
+
+def filepath(filename):
+    '''Determine the path to a file in the data directory.
+    '''
+    return os.path.join(data_dir, filename)
+
+
+def load(filename, mode='rb'):
+    '''Open a file in the data directory.
+
+    "mode" is passed as the second arg to open().
+    '''
+    return open(os.path.join(data_dir, filename), mode)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/endscreen.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,33 @@
+# endscreen.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+# Victory screen for the game
+
+from albow.screen import Screen
+from albow.resource import get_image
+
+from gamelib.widgets import BoomImageButton
+
+
+class EndImageButton(BoomImageButton):
+
+    FOLDER = 'won'
+
+
+class EndScreen(Screen):
+    def __init__(self, shell):
+        Screen.__init__(self, shell)
+        self.background = get_image('won', 'won.png')
+        self._menu_button = EndImageButton('menu.png', 26, 500,
+                action=self.main_menu)
+        self._quit_button = EndImageButton('quit.png', 250, 500,
+                action=shell.quit)
+        self.add(self._menu_button)
+        self.add(self._quit_button)
+
+    def draw(self, surface):
+        surface.blit(self.background, (0, 0))
+        self._menu_button.draw(surface)
+        self._quit_button.draw(surface)
+
+    def main_menu(self):
+        self.shell.show_screen(self.shell.menu_screen)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/gamescreen.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,261 @@
+# gamescreen.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+# Main menu for the game
+
+from albow.controls import Widget
+from albow.layout import Row
+from albow.palette_view import PaletteView
+from albow.screen import Screen
+from pygame import Rect, mouse
+from pygame.color import Color
+
+from constants import SCREEN, BUTTON_SIZE, SCENE_SIZE, LEAVE
+from cursor import CursorWidget
+from state import initial_state, handle_result
+from widgets import (MessageDialog, BoomButton, HandButton, PopupMenu,
+                     PopupMenuButton)
+
+
+class InventoryView(PaletteView):
+
+    sel_color = Color("yellow")
+    sel_width = 2
+
+    def __init__(self, screen):
+        PaletteView.__init__(self, (BUTTON_SIZE, BUTTON_SIZE), 1, 6,
+                             scrolling=True)
+        self.screen = screen
+        self.state = screen.state
+        self.state_widget = screen.state_widget
+
+    def num_items(self):
+        return len(self.state.inventory)
+
+    def draw_item(self, surface, item_no, rect):
+        item_image = self.state.inventory[item_no].get_inventory_image()
+        surface.blit(item_image, rect, None)
+
+    def click_item(self, item_no, event):
+        item = self.state.inventory[item_no]
+        if self.item_is_selected(item_no):
+            self.unselect()
+        elif item.is_interactive(self.state.tool):
+            result = item.interact(self.state.tool)
+            handle_result(result, self.state_widget)
+        else:
+            self.state.set_tool(self.state.inventory[item_no])
+
+    def mouse_down(self, event):
+        if event.button != 1:
+            self.state.cancel_doodah(self.screen)
+        else:
+            PaletteView.mouse_down(self, event)
+
+    def item_is_selected(self, item_no):
+        return self.state.tool is self.state.inventory[item_no]
+
+    def unselect(self):
+        self.state.set_tool(None)
+
+
+class StateWidget(Widget):
+
+    def __init__(self, screen):
+        Widget.__init__(self, Rect(0, 0, SCENE_SIZE[0], SCENE_SIZE[1]))
+        self.screen = screen
+        self.state = screen.state
+        self.detail = DetailWindow(screen)
+
+    def draw(self, surface):
+        if self.state.previous_scene and self.state.do_check == LEAVE:
+            # We still need to handle leave events, so still display the scene
+            self.state.previous_scene.draw(surface, self)
+        else:
+            self.state.current_scene.draw(surface, self)
+
+    def mouse_down(self, event):
+        self.mouse_move(event)
+        if event.button != 1:  # We have a right/middle click
+            self.state.cancel_doodah(self.screen)
+        elif self.subwidgets:
+            self.clear_detail()
+            self._mouse_move(event.pos)
+        else:
+            result = self.state.interact(event.pos)
+            handle_result(result, self)
+
+    def animate(self):
+        if self.state.animate():
+            # queue a redraw
+            self.invalidate()
+        # We do this here so we can get enter and leave events regardless
+        # of what happens
+        result = self.state.check_enter_leave(self.screen)
+        handle_result(result, self)
+
+    def mouse_move(self, event):
+        self.state.highlight_override = False
+        if not self.subwidgets:
+            self._mouse_move(event.pos)
+
+    def _mouse_move(self, pos):
+        self.state.highlight_override = False
+        self.state.current_scene.mouse_move(pos)
+        self.state.old_pos = pos
+
+    def show_message(self, message, style=None):
+        # Display the message as a modal dialog
+        MessageDialog(self.screen, message, 60, style=style).present()
+        # queue a redraw to show updated state
+        self.invalidate()
+        # The cursor could have gone anywhere
+        if self.subwidgets:
+            self.subwidgets[0]._mouse_move(mouse.get_pos())
+        else:
+            self._mouse_move(mouse.get_pos())
+
+    def show_detail(self, detail):
+        self.clear_detail()
+        detail_obj = self.state.set_current_detail(detail)
+        self.detail.set_image_rect(Rect((0, 0), detail_obj.get_detail_size()))
+        self.add_centered(self.detail)
+        self.state.do_enter_detail()
+
+    def clear_detail(self):
+        """Hide the detail view"""
+        if self.state.current_detail is not None:
+            self.remove(self.detail)
+            self.state.do_leave_detail()
+            self.state.set_current_detail(None)
+            self._mouse_move(mouse.get_pos())
+
+    def end_game(self):
+        self.screen.running = False
+        self.screen.shell.show_screen(self.screen.shell.end_screen)
+
+
+class DetailWindow(Widget):
+    def __init__(self, screen):
+        Widget.__init__(self)
+        self.image_rect = None
+        self.screen = screen
+        self.state = screen.state
+        self.border_width = 5
+        self.border_color = (0, 0, 0)
+        # parent only gets set when we get added to the scene
+        self.close = BoomButton('Close', self.close_but, screen)
+        self.add(self.close)
+
+    def close_but(self):
+        self.parent.clear_detail()
+
+    def end_game(self):
+        self.parent.end_game()
+
+    def set_image_rect(self, rect):
+        bw = self.border_width
+        self.image_rect = rect
+        self.image_rect.topleft = (bw, bw)
+        self.set_rect(rect.inflate(bw * 2, bw * 2))
+        self.close.rect.midbottom = rect.midbottom
+
+    def draw(self, surface):
+        scene_surface = self.get_root().surface.subsurface(self.parent.rect)
+        overlay = scene_surface.convert_alpha()
+        overlay.fill(Color(0, 0, 0, 191))
+        scene_surface.blit(overlay, (0, 0))
+        self.state.current_detail.draw(
+            surface.subsurface(self.image_rect), self)
+
+    def mouse_down(self, event):
+        self.mouse_move(event)
+        if event.button != 1:  # We have a right/middle click
+            self.state.cancel_doodah(self.screen)
+        else:
+            result = self.state.interact_detail(
+                self.global_to_local(event.pos))
+            handle_result(result, self)
+
+    def mouse_move(self, event):
+        self._mouse_move(event.pos)
+
+    def _mouse_move(self, pos):
+        self.state.highlight_override = False
+        self.state.current_detail.mouse_move(self.global_to_local(pos))
+
+    def show_message(self, message, style=None):
+        self.parent.show_message(message, style)
+        self.invalidate()
+
+
+class ToolBar(Row):
+    def __init__(self, items):
+        for item in items:
+            item.height = BUTTON_SIZE
+        Row.__init__(self, items, spacing=0, width=SCREEN[0])
+        self.bg_color = (31, 31, 31)
+
+    def draw(self, surface):
+        surface.fill(self.bg_color)
+        Row.draw(self, surface)
+
+
+class GameScreen(Screen, CursorWidget):
+    def __init__(self, shell):
+        CursorWidget.__init__(self, self)
+        Screen.__init__(self, shell)
+        self.running = False
+
+    def _clear_all(self):
+        for widget in self.subwidgets[:]:
+            self.remove(widget)
+
+    def start_game(self):
+        self._clear_all()
+        self.state = initial_state()
+        self.state_widget = StateWidget(self)
+        self.add(self.state_widget)
+
+        self.popup_menu = PopupMenu(self)
+        self.menubutton = PopupMenuButton('Menu',
+                action=self.popup_menu.show_menu)
+
+        self.handbutton = HandButton(action=self.hand_pressed)
+
+        self.inventory = InventoryView(self)
+
+        self.toolbar = ToolBar([
+                self.menubutton,
+                self.handbutton,
+                self.inventory,
+                ])
+        self.toolbar.bottomleft = self.bottomleft
+        self.add(self.toolbar)
+
+        self.running = True
+
+    def enter_screen(self):
+        CursorWidget.enter_screen(self)
+
+    def leave_screen(self):
+        CursorWidget.leave_screen(self)
+
+    # Albow uses magic method names (command + '_cmd'). Yay.
+    # Albow's search order means they need to be defined here, not in
+    # PopMenu, which is annoying.
+    def hide_cmd(self):
+        # This option does nothing, but the method needs to exist for albow
+        return
+
+    def main_menu_cmd(self):
+        self.shell.show_screen(self.shell.menu_screen)
+
+    def quit_cmd(self):
+        self.shell.quit()
+
+    def hand_pressed(self):
+        self.inventory.unselect()
+
+    def begin_frame(self):
+        if self.running:
+            self.state_widget.animate()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/main.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,77 @@
+'''Game main module.
+
+Contains the entry point used by the run_game.py script.
+
+'''
+
+# Albow looks for stuff in os.path[0], which isn't always where it expects.
+# The following horribleness fixes this.
+import sys
+import os.path
+right_path = os.path.dirname(os.path.dirname(__file__))
+sys.path.insert(0, right_path)
+from optparse import OptionParser
+
+import pygame
+from pygame.locals import SWSURFACE
+from albow.shell import Shell
+
+from menu import MenuScreen
+from gamescreen import GameScreen
+from endscreen import EndScreen
+from constants import (
+    SCREEN, FRAME_RATE, FREQ, BITSIZE, CHANNELS, BUFFER, DEBUG)
+from sound import no_sound, disable_sound
+import state
+import data
+
+
+def parse_args(args):
+    parser = OptionParser()
+    parser.add_option("--no-sound", action="store_false", default=True,
+            dest="sound", help="disable sound")
+    if DEBUG:
+        parser.add_option("--scene", type="str", default=None,
+            dest="scene", help="initial scene")
+        parser.add_option("--no-rects", action="store_false", default=True,
+            dest="rects", help="disable debugging rects")
+    opts, _ = parser.parse_args(args or [])
+    return opts
+
+
+class MainShell(Shell):
+    def __init__(self, display):
+        Shell.__init__(self, display)
+        self.menu_screen = MenuScreen(self)
+        self.game_screen = GameScreen(self)
+        self.end_screen = EndScreen(self)
+        self.set_timer(FRAME_RATE)
+        self.show_screen(self.menu_screen)
+
+
+def main():
+    opts = parse_args(sys.argv)
+    pygame.display.init()
+    pygame.font.init()
+    if opts.sound:
+        try:
+            pygame.mixer.init(FREQ, BITSIZE, CHANNELS, BUFFER)
+        except pygame.error, exc:
+            no_sound(exc)
+    else:
+        # Ensure get_sound returns nothing, so everything else just works
+        disable_sound()
+    if DEBUG:
+        if opts.scene is not None:
+            # debug the specified scene
+            state.DEBUG_SCENE = opts.scene
+        state.DEBUG_RECTS = opts.rects
+    display = pygame.display.set_mode(SCREEN, SWSURFACE)
+    pygame.display.set_icon(pygame.image.load(
+        data.filepath('icons/suspended_sentence24x24.png')))
+    pygame.display.set_caption("Suspended Sentence")
+    shell = MainShell(display)
+    try:
+        shell.run()
+    except KeyboardInterrupt:
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/menu.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,43 @@
+# menu.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+# Main menu for the game
+
+from albow.screen import Screen
+from albow.resource import get_image
+
+from gamelib.widgets import BoomImageButton
+
+
+class SplashButton(BoomImageButton):
+
+    FOLDER = 'splash'
+
+
+class MenuScreen(Screen):
+    def __init__(self, shell):
+        Screen.__init__(self, shell)
+        self._background = get_image('splash', 'splash.png')
+        self._start_button = SplashButton('play.png', 16, 523, self.start)
+        self._resume_button = SplashButton('resume.png', 256, 523, self.resume,
+                                           enable=self.check_running)
+        self._quit_button = SplashButton('quit.png', 580, 523, shell.quit)
+        self.add(self._start_button)
+        self.add(self._resume_button)
+        self.add(self._quit_button)
+
+    def draw(self, surface):
+        surface.blit(self._background, (0, 0))
+        self._start_button.draw(surface)
+        self._resume_button.draw(surface)
+        self._quit_button.draw(surface)
+
+    def start(self):
+        self.shell.game_screen.start_game()
+        self.shell.show_screen(self.shell.game_screen)
+
+    def check_running(self):
+        return self.shell.game_screen.running
+
+    def resume(self):
+        if self.shell.game_screen.running:
+            self.shell.show_screen(self.shell.game_screen)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/scenewidgets.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,149 @@
+"""Interactive elements within a Scene."""
+
+
+from pygame import Rect
+from pygame.color import Color
+from pygame.colordict import THECOLORS
+from pygame.surface import Surface
+from albow.resource import get_image
+
+from gamelib.state import Thing
+from gamelib.constants import DEBUG
+from gamelib.widgets import BoomLabel
+
+
+class Interact(object):
+
+    def __init__(self, image, rect, interact_rect):
+        self.image = image
+        self.rect = rect
+        self.interact_rect = interact_rect
+
+    def set_thing(self, thing):
+        pass
+
+    def draw(self, surface):
+        if self.image is not None:
+            surface.blit(self.image, self.rect, None)
+
+    def animate(self):
+        return False
+
+
+class InteractNoImage(Interact):
+
+    def __init__(self, x, y, w, h):
+        super(InteractNoImage, self).__init__(None, None, Rect(x, y, w, h))
+
+
+class InteractText(Interact):
+    """Display box with text to interact with -- mostly for debugging."""
+
+    def __init__(self, x, y, text, bg_color=None):
+        if bg_color is None:
+            bg_color = (127, 127, 127)
+        label = BoomLabel(text)
+        label.set_margin(5)
+        label.border_width = 1
+        label.border_color = (0, 0, 0)
+        label.bg_color = bg_color
+        label.fg_color = (0, 0, 0)
+        image = Surface(label.size)
+        rect = Rect((x, y), label.size)
+        label.draw_all(image)
+        super(InteractText, self).__init__(image, rect, rect)
+
+
+class InteractRectUnion(Interact):
+
+    def __init__(self, rect_list):
+        # pygame.rect.Rect.unionall should do this, but is broken
+        # in some pygame versions (including 1.8, it appears)
+        rect_list = [Rect(x) for x in rect_list]
+        union_rect = rect_list[0]
+        for rect in rect_list[1:]:
+            union_rect = union_rect.union(rect)
+        super(InteractRectUnion, self).__init__(None, None, union_rect)
+        self.interact_rect = rect_list
+
+
+class InteractImage(Interact):
+
+    def __init__(self, x, y, image_name):
+        super(InteractImage, self).__init__(None, None, None)
+        self._pos = (x, y)
+        self._image_name = image_name
+
+    def set_thing(self, thing):
+        self.image = get_image(thing.folder, self._image_name)
+        self.rect = Rect(self._pos, self.image.get_size())
+        self.interact_rect = self.rect
+
+
+class InteractImageRect(InteractImage):
+    def __init__(self, x, y, image_name, r_x, r_y, r_w, r_h):
+        super(InteractImageRect, self).__init__(x, y, image_name)
+        self._r_pos = (r_x, r_y)
+        self._r_size = (r_w, r_h)
+
+    def set_thing(self, thing):
+        super(InteractImageRect, self).set_thing(thing)
+        self.interact_rect = Rect(self._r_pos, self._r_size)
+
+
+class InteractAnimated(Interact):
+    """Interactive with an animation rather than an image"""
+
+    # FIXME: Assumes all images are the same size
+    # anim_seq - sequence of image names
+    # delay - number of frames to wait between changing images
+
+    def __init__(self, x, y, anim_seq, delay):
+        self._pos = (x, y)
+        self._anim_pos = 0
+        self._names = anim_seq
+        self._frame_count = 0
+        self._anim_seq = None
+        self._delay = delay
+
+    def set_thing(self, thing):
+        self._anim_seq = [get_image(thing.folder, x) for x in self._names]
+        self.image = self._anim_seq[0]
+        self.rect = Rect(self._pos, self.image.get_size())
+        self.interact_rect = self.rect
+
+    def animate(self):
+        if self._anim_seq:
+            self._frame_count += 1
+            if self._frame_count > self._delay:
+                self._frame_count = 0
+                self._anim_pos += 1
+                if self._anim_pos >= len(self._anim_seq):
+                    self._anim_pos = 0
+                self.image = self._anim_seq[self._anim_pos]
+                # queue redraw
+                return True
+        return False
+
+
+class GenericDescThing(Thing):
+    "Thing with an InteractiveUnionRect and a description"
+
+    INITIAL = "description"
+
+    def __init__(self, prefix, number, description, areas):
+        super(GenericDescThing, self).__init__()
+        self.description = description
+        self.name = '%s.%s' % (prefix, number)
+        self.interacts = {
+                'description': InteractRectUnion(areas)
+                }
+        if DEBUG:
+            # Individual colors to make debugging easier
+            self._interact_hilight_color = Color(THECOLORS.keys()[number])
+
+    def get_description(self):
+        return self.description
+
+    def is_interactive(self, tool=None):
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/sound.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,75 @@
+# Sound management for Suspended Sentence
+
+# This re-implements some of the albow.resource code to
+# a) work around an annoying bugs
+# b) add some missing functionality (disable_sound)
+
+import os
+
+import pygame
+from pygame.mixer import music
+from albow.resource import _resource_path, dummy_sound
+import albow.music
+
+sound_cache = {}
+
+
+def get_sound(*names):
+    if sound_cache is None:
+        return dummy_sound
+    path = _resource_path("sounds", names)
+    sound = sound_cache.get(path)
+    if not sound:
+        if not os.path.isfile(path):
+            missing_sound("File does not exist", path)
+            return dummy_sound
+        try:
+            from pygame.mixer import Sound
+        except ImportError, e:
+            no_sound(e)
+            return dummy_sound
+        try:
+            sound = Sound(path)
+        except pygame.error, e:
+            missing_sound(e, path)
+            return dummy_sound
+        sound_cache[path] = sound
+    return sound
+
+
+def no_sound(e):
+    global sound_cache
+    print "get_sound: %s" % e
+    print "get_sound: Sound not available, continuing without it"
+    sound_cache = None
+    albow.music.music_enabled = False
+
+
+def disable_sound():
+    global sound_cache
+    sound_cache = None
+    albow.music.music_enabled = False
+
+
+def missing_sound(e, name):
+    print "albow.resource.get_sound: %s: %s" % (name, e)
+
+
+def start_next_music():
+    """Start playing the next item from the current playlist immediately."""
+    if albow.music.music_enabled and albow.music.current_playlist:
+        next_music = albow.music.current_playlist.next()
+        if next_music:
+            #print "albow.music: loading", repr(next_music)
+            music.load(next_music)
+            music.play()
+            albow.music.next_change_delay = albow.music.change_delay
+        albow.music.current_music = next_music
+
+
+def get_current_playlist():
+    if albow.music.music_enabled and albow.music.current_playlist:
+        return albow.music.current_playlist
+
+# Monkey patch
+albow.music.start_next_music = start_next_music
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/speech.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,44 @@
+# speech.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+# Speech playing and cache
+
+import re
+
+from sound import get_sound
+
+
+# cache of string -> sound object mappings
+_SPEECH_CACHE = {}
+
+# characters not to allow in filenames
+_REPLACE_RE = re.compile(r"[^a-z0-9-]+")
+
+
+class SpeechError(RuntimeError):
+    pass
+
+
+def get_filename(key, text):
+    """Simplify text to filename."""
+    filename = "%s-%s" % (key, text)
+    filename = filename.lower()
+    filename = _REPLACE_RE.sub("_", filename)
+    filename = filename[:30]
+    filename = "%s.ogg" % filename
+    return filename
+
+
+def get_speech(thing_name, text):
+    """Load a sound object from the cache."""
+    key = (thing_name, text)
+    if key in _SPEECH_CACHE:
+        return _SPEECH_CACHE[key]
+    filename = get_filename(thing_name, text)
+    _SPEECH_CACHE[key] = sound = get_sound("speech", filename)
+    return sound
+
+
+def say(thing_name, text):
+    """Play text as speech."""
+    sound = get_speech(thing_name, text)
+    sound.play()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/state.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,548 @@
+"""Utilities and base classes for dealing with scenes."""
+
+import copy
+
+from albow.resource import get_image
+from albow.utils import frame_rect
+from widgets import BoomLabel
+from pygame.rect import Rect
+from pygame.color import Color
+
+import constants
+from scenes import SCENE_LIST, INITIAL_SCENE
+from sound import get_sound
+
+# override the initial scene to for debugging
+DEBUG_SCENE = None
+
+# whether to show debugging rects
+DEBUG_RECTS = False
+
+
+class Result(object):
+    """Result of interacting with a thing"""
+
+    def __init__(self, message=None, soundfile=None, detail_view=None,
+                 style=None, close_detail=False, end_game=False):
+        self.message = message
+        self.sound = None
+        if soundfile:
+            self.sound = get_sound(soundfile)
+        self.detail_view = detail_view
+        self.style = style
+        self.close_detail = close_detail
+        self.end_game = end_game
+
+    def process(self, scene_widget):
+        """Helper function to do the right thing with a result object"""
+        if self.sound:
+            self.sound.play()
+        if self.message:
+            scene_widget.show_message(self.message, self.style)
+        if self.detail_view:
+            scene_widget.show_detail(self.detail_view)
+        if (self.close_detail
+            and hasattr(scene_widget, 'parent')
+            and hasattr(scene_widget.parent, 'clear_detail')):
+            scene_widget.parent.clear_detail()
+        if self.end_game:
+            scene_widget.end_game()
+
+
+def handle_result(result, scene_widget):
+    """Handle dealing with result or result sequences"""
+    if result:
+        if hasattr(result, 'process'):
+            result.process(scene_widget)
+        else:
+            for res in result:
+                if res:
+                    # List may contain None's
+                    res.process(scene_widget)
+
+
+def initial_state():
+    """Load the initial state."""
+    state = GameState()
+    for scene in SCENE_LIST:
+        state.load_scenes(scene)
+    initial_scene = INITIAL_SCENE if DEBUG_SCENE is None else DEBUG_SCENE
+    state.set_current_scene(initial_scene)
+    state.set_do_enter_leave()
+    return state
+
+
+class GameState(object):
+    """Complete game state.
+
+    Game state consists of:
+
+    * items
+    * scenes
+    """
+
+    def __init__(self):
+        # map of scene name -> Scene object
+        self.scenes = {}
+        # map of detail view name -> DetailView object
+        self.detail_views = {}
+        # map of item name -> Item object
+        self.items = {}
+        # list of item objects in inventory
+        self.inventory = []
+        # currently selected tool (item)
+        self.tool = None
+        # current scene
+        self.current_scene = None
+        # current detail view
+        self.current_detail = None
+        # scene we came from, for enter and leave processing
+        self.previous_scene = None
+        # scene transion helpers
+        self.do_check = None
+        self.old_pos = None
+        # current thing
+        self.current_thing = None
+        self.highlight_override = False
+
+    def add_scene(self, scene):
+        self.scenes[scene.name] = scene
+
+    def add_detail_view(self, detail_view):
+        self.detail_views[detail_view.name] = detail_view
+
+    def add_item(self, item):
+        self.items[item.name] = item
+        item.set_state(self)
+
+    def load_scenes(self, modname):
+        mod = __import__("gamelib.scenes.%s" % (modname,), fromlist=[modname])
+        for scene_cls in mod.SCENES:
+            self.add_scene(scene_cls(self))
+        if hasattr(mod, 'DETAIL_VIEWS'):
+            for scene_cls in mod.DETAIL_VIEWS:
+                self.add_detail_view(scene_cls(self))
+
+    def set_current_scene(self, name):
+        old_scene = self.current_scene
+        self.current_scene = self.scenes[name]
+        self.current_thing = None
+        if old_scene and old_scene != self.current_scene:
+            self.previous_scene = old_scene
+            self.set_do_enter_leave()
+
+    def set_current_detail(self, name):
+        self.current_thing = None
+        if name is None:
+            self.current_detail = None
+        else:
+            self.current_detail = self.detail_views[name]
+            return self.current_detail
+
+    def add_inventory_item(self, name):
+        self.inventory.append(self.items[name])
+
+    def is_in_inventory(self, name):
+        if name in self.items:
+            return self.items[name] in self.inventory
+        return False
+
+    def remove_inventory_item(self, name):
+        self.inventory.remove(self.items[name])
+        # Unselect tool if it's removed
+        if self.tool == self.items[name]:
+            self.set_tool(None)
+
+    def replace_inventory_item(self, old_item_name, new_item_name):
+        """Try to replace an item in the inventory with a new one"""
+        try:
+            index = self.inventory.index(self.items[old_item_name])
+            self.inventory[index] = self.items[new_item_name]
+            if self.tool == self.items[old_item_name]:
+                self.set_tool(self.items[new_item_name])
+        except ValueError:
+            return False
+        return True
+
+    def set_tool(self, item):
+        self.tool = item
+
+    def interact(self, pos):
+        return self.current_scene.interact(self.tool, pos)
+
+    def interact_detail(self, pos):
+        return self.current_detail.interact(self.tool, pos)
+
+    def cancel_doodah(self, screen):
+        if self.tool:
+            self.set_tool(None)
+        elif self.current_detail:
+            screen.state_widget.clear_detail()
+
+    def do_enter_detail(self):
+        if self.current_detail:
+            self.current_detail.enter()
+
+    def do_leave_detail(self):
+        if self.current_detail:
+            self.current_detail.leave()
+
+    def animate(self):
+        if not self.do_check:
+            return self.current_scene.animate()
+
+    def check_enter_leave(self, screen):
+        if not self.do_check:
+            return None
+        if self.do_check == constants.LEAVE:
+            self.do_check = constants.ENTER
+            if self.previous_scene:
+                return self.previous_scene.leave()
+            return None
+        elif self.do_check == constants.ENTER:
+            self.do_check = None
+            # Fix descriptions, etc.
+            if self.old_pos:
+                self.current_scene.update_current_thing(self.old_pos)
+            return self.current_scene.enter()
+        raise RuntimeError('invalid do_check value %s' % self.do_check)
+
+    def set_do_enter_leave(self):
+        """Flag that we need to run the enter loop"""
+        self.do_check = constants.LEAVE
+
+
+class StatefulGizmo(object):
+
+    # initial data (optional, defaults to none)
+    INITIAL_DATA = None
+
+    def __init__(self):
+        self.data = {}
+        if self.INITIAL_DATA:
+            # deep copy of INITIAL_DATA allows lists, sets and
+            # other mutable types to safely be used in INITIAL_DATA
+            self.data.update(copy.deepcopy(self.INITIAL_DATA))
+
+    def set_data(self, key, value):
+        self.data[key] = value
+
+    def get_data(self, key):
+        return self.data.get(key, None)
+
+
+class Scene(StatefulGizmo):
+    """Base class for scenes."""
+
+    # sub-folder to look for resources in
+    FOLDER = None
+
+    # name of background image resource
+    BACKGROUND = None
+
+    # name of scene (optional, defaults to folder)
+    NAME = None
+
+    # Offset of the background image
+    OFFSET = (0, 0)
+
+    def __init__(self, state):
+        StatefulGizmo.__init__(self)
+        # scene name
+        self.name = self.NAME if self.NAME is not None else self.FOLDER
+        # link back to state object
+        self.state = state
+        # map of thing names -> Thing objects
+        self.things = {}
+        self._background = None
+
+    def add_item(self, item):
+        self.state.add_item(item)
+
+    def add_thing(self, thing):
+        self.things[thing.name] = thing
+        thing.set_scene(self)
+
+    def remove_thing(self, thing):
+        del self.things[thing.name]
+        if thing is self.state.current_thing:
+            self.state.current_thing.leave()
+            self.state.current_thing = None
+
+    def _get_description(self):
+        text = (self.state.current_thing and
+                self.state.current_thing.get_description())
+        if text is None:
+            return None
+        label = BoomLabel(text)
+        label.set_margin(5)
+        label.border_width = 1
+        label.border_color = (0, 0, 0)
+        label.bg_color = Color(210, 210, 210, 255)
+        label.fg_color = (0, 0, 0)
+        return label
+
+    def draw_description(self, surface, screen):
+        description = self._get_description()
+        if description is not None:
+            w, h = description.size
+            sub = screen.get_root().surface.subsurface(
+                Rect(400 - w / 2, 5, w, h))
+            description.draw_all(sub)
+
+    def _cache_background(self):
+        if self.BACKGROUND and not self._background:
+            self._background = get_image(self.FOLDER, self.BACKGROUND)
+
+    def draw_background(self, surface):
+        self._cache_background()
+        if self._background is not None:
+            surface.blit(self._background, self.OFFSET, None)
+        else:
+            surface.fill((200, 200, 200))
+
+    def draw_things(self, surface):
+        for thing in self.things.itervalues():
+            thing.draw(surface)
+
+    def draw(self, surface, screen):
+        self.draw_background(surface)
+        self.draw_things(surface)
+        self.draw_description(surface, screen)
+
+    def interact(self, item, pos):
+        """Interact with a particular position.
+
+        Item may be an item in the list of items or None for the hand.
+
+        Returns a Result object to provide feedback to the player.
+        """
+        if self.state.current_thing is not None:
+            return self.state.current_thing.interact(item)
+
+    def animate(self):
+        """Animate all the things in the scene.
+
+           Return true if any of them need to queue a redraw"""
+        result = False
+        for thing in self.things.itervalues():
+            if thing.animate():
+                result = True
+        return result
+
+    def enter(self):
+        return None
+
+    def leave(self):
+        return None
+
+    def update_current_thing(self, pos):
+        if self.state.current_thing is not None:
+            if not self.state.current_thing.contains(pos):
+                self.state.current_thing.leave()
+                self.state.current_thing = None
+        for thing in self.things.itervalues():
+            if thing.contains(pos):
+                thing.enter(self.state.tool)
+                self.state.current_thing = thing
+                break
+
+    def mouse_move(self, pos):
+        """Call to check whether the cursor has entered / exited a thing.
+
+        Item may be an item in the list of items or None for the hand.
+        """
+        self.update_current_thing(pos)
+
+    def get_detail_size(self):
+        self._cache_background()
+        return self._background.get_size()
+
+
+class InteractiveMixin(object):
+    def is_interactive(self, tool=None):
+        return True
+
+    def interact(self, tool):
+        if not self.is_interactive(tool):
+            return None
+        if tool is None:
+            return self.interact_without()
+        handler = getattr(self, 'interact_with_' + tool.tool_name, None)
+        inverse_handler = self.get_inverse_interact(tool)
+        if handler is not None:
+            return handler(tool)
+        elif inverse_handler is not None:
+            return inverse_handler(self)
+        else:
+            return self.interact_default(tool)
+
+    def get_inverse_interact(self, tool):
+        return None
+
+    def interact_without(self):
+        return self.interact_default(None)
+
+    def interact_default(self, item=None):
+        return None
+
+
+class Thing(StatefulGizmo, InteractiveMixin):
+    """Base class for things in a scene that you can interact with."""
+
+    # name of thing
+    NAME = None
+
+    # sub-folder to look for resources in (defaults to scenes folder)
+    FOLDER = None
+
+    # list of Interact objects
+    INTERACTS = {}
+
+    # name first interact
+    INITIAL = None
+
+    # Interact rectangle hi-light color (for debugging)
+    # (set to None to turn off)
+    _interact_hilight_color = Color('red')
+
+    def __init__(self):
+        StatefulGizmo.__init__(self)
+        # name of the thing
+        self.name = self.NAME
+        # folder for resource (None is overridden by scene folder)
+        self.folder = self.FOLDER
+        # interacts
+        self.interacts = self.INTERACTS
+        # these are set by set_scene
+        self.scene = None
+        self.state = None
+        self.current_interact = None
+        self.rect = None
+        self.orig_rect = None
+
+    def _fix_rect(self):
+        """Fix rects to compensate for scene offset"""
+        # Offset logic is to always work with copies, to avoid
+        # flying effects from multiple calls to _fix_rect
+        # See footwork in draw
+        if hasattr(self.rect, 'collidepoint'):
+            self.rect = self.rect.move(self.scene.OFFSET)
+        else:
+            self.rect = [x.move(self.scene.OFFSET) for x in self.rect]
+
+    def set_scene(self, scene):
+        assert self.scene is None
+        self.scene = scene
+        if self.folder is None:
+            self.folder = scene.FOLDER
+        self.state = scene.state
+        for interact in self.interacts.itervalues():
+            interact.set_thing(self)
+        self.set_interact(self.INITIAL)
+
+    def set_interact(self, name):
+        self.current_interact = self.interacts[name]
+        self.rect = self.current_interact.interact_rect
+        if self.scene:
+            self._fix_rect()
+        assert self.rect is not None, name
+
+    def contains(self, pos):
+        if hasattr(self.rect, 'collidepoint'):
+            return self.rect.collidepoint(pos)
+        else:
+            # FIXME: add sanity check
+            for rect in list(self.rect):
+                if rect.collidepoint(pos):
+                    return True
+        return False
+
+    def get_description(self):
+        return None
+
+    def enter(self, item):
+        """Called when the cursor enters the Thing."""
+        pass
+
+    def leave(self):
+        """Called when the cursr leaves the Thing."""
+        pass
+
+    def animate(self):
+        return self.current_interact.animate()
+
+    def draw(self, surface):
+        old_rect = self.current_interact.rect
+        if old_rect:
+            self.current_interact.rect = old_rect.move(self.scene.OFFSET)
+        self.current_interact.draw(surface)
+        self.current_interact.rect = old_rect
+        if DEBUG_RECTS and self._interact_hilight_color:
+            if hasattr(self.rect, 'collidepoint'):
+                frame_rect(surface, self._interact_hilight_color,
+                        self.rect.inflate(1, 1), 1)
+            else:
+                for rect in self.rect:
+                    frame_rect(surface, self._interact_hilight_color,
+                            rect.inflate(1, 1), 1)
+
+
+class Item(InteractiveMixin):
+    """Base class for inventory items."""
+
+    # image for inventory
+    INVENTORY_IMAGE = None
+
+    # name of item
+    NAME = None
+
+    # name for interactions (i.e. def interact_with_<TOOL_NAME>)
+    TOOL_NAME = None
+
+    # set to instance of CursorSprite
+    CURSOR = None
+
+    def __init__(self, name=None):
+        self.state = None
+        self.name = self.NAME
+        if name is not None:
+            self.name = name
+        self.tool_name = name
+        if self.TOOL_NAME is not None:
+            self.tool_name = self.TOOL_NAME
+        self.inventory_image = None
+
+    def _cache_inventory_image(self):
+        if not self.inventory_image:
+            self.inventory_image = get_image('items', self.INVENTORY_IMAGE)
+
+    def set_state(self, state):
+        assert self.state is None
+        self.state = state
+
+    def get_inventory_image(self):
+        self._cache_inventory_image()
+        return self.inventory_image
+
+    def get_inverse_interact(self, tool):
+        return getattr(tool, 'interact_with_' + self.tool_name, None)
+
+    def is_interactive(self, tool=None):
+        if tool:
+            return True
+        return False
+
+
+class CloneableItem(Item):
+    _counter = 0
+
+    @classmethod
+    def _get_new_id(cls):
+        cls._counter += 1
+        return cls._counter - 1
+
+    def __init__(self, name=None):
+        super(CloneableItem, self).__init__(name)
+        my_count = self._get_new_id()
+        self.name = "%s.%s" % (self.name, my_count)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/version.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,82 @@
+"""Suspended Sentence Version Information"""
+
+VERSION = (1, 1, 0, 'alpha', 0)
+BASE_VERSION_STR = '.'.join([str(x) for x in VERSION[:3]])
+VERSION_STR = {
+    'final': BASE_VERSION_STR,
+    'alpha': BASE_VERSION_STR + 'a' + str(VERSION[4]),
+    'rc': BASE_VERSION_STR + 'rc' + str(VERSION[4]),
+}[VERSION[3]]
+
+NAME = 'Suspended Sentence'
+DESCRIPTION = 'Point-and-click adventure game written using Pygame.'
+
+PEOPLE = {
+    'Simon': ('Simon Cross', 'hodgestar+rinkhals@gmail.com'),
+    'Neil': ('Neil Muller', 'drnmuller+rinkhals@gmail.com'),
+    'Adrianna': ('Adrianna Pinska', 'adrianna.pinska+rinkhals@gmail.com'),
+    'Jeremy': ('Jeremy Thurgood', 'firxen+rinkhals@gmail.com'),
+    'Stefano': ('Stefano Rivera', 'stefano@rivera.za.net'),
+}
+
+AUTHORS = [
+    PEOPLE['Simon'],
+    PEOPLE['Neil'],
+    PEOPLE['Adrianna'],
+    PEOPLE['Jeremy'],
+    PEOPLE['Stefano'],
+]
+
+AUTHOR_NAME = AUTHORS[0][0]
+AUTHOR_EMAIL = AUTHORS[0][1]
+
+MAINTAINERS = AUTHORS
+
+MAINTAINER_NAME = MAINTAINERS[0][0]
+MAINTAINER_EMAIL = MAINTAINERS[0][1]
+
+ARTISTS = [
+    PEOPLE['Adrianna'],
+]
+
+DOCUMENTERS = [
+    PEOPLE['Simon'],
+]
+
+# SOURCEFORGE_URL = 'http://sourceforge.net/projects/XXXX/'
+# PYPI_URL = 'http://pypi.python.org/pypi/XXXX/'
+
+LICENSE = 'MIT'
+# LICENSE_TEXT = resource_string(__name__, 'COPYING')
+
+CLASSIFIERS = [
+    'Development Status :: 4 - Beta',
+    'Environment :: MacOS X',
+    'Environment :: Win32 (MS Windows)',
+    'Environment :: X11 Applications',
+    'Intended Audience :: End Users/Desktop',
+    'License :: OSI Approved :: MIT License',
+    'Natural Language :: English',
+    'Operating System :: Microsoft :: Windows',
+    'Operating System :: POSIX',
+    'Operating System :: MacOS :: MacOS X',
+    'Programming Language :: Python :: 2.5',
+    'Programming Language :: Python :: 2.6',
+    'Topic :: Games/Entertainment :: Role-Playing',
+]
+
+PLATFORMS = [
+    'Linux',
+    'Mac OS X',
+    'Windows',
+]
+
+INSTALL_REQUIRES = [
+]
+
+# Install these manually
+NON_EGG_REQUIREMENTS = [
+    'setuptools',
+    'pygame',
+    'albow',
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyntnclick/widgets.py	Sat Feb 11 13:10:18 2012 +0200
@@ -0,0 +1,215 @@
+# widgets.py
+# Copyright Boomslang team, 2010 (see COPYING File)
+
+"""Custom Albow widgets"""
+
+import textwrap
+
+import albow.controls
+import albow.menu
+from albow.resource import get_font, get_image
+from pygame.color import Color
+from pygame.rect import Rect
+from pygame.draw import lines as draw_lines
+from pygame import mouse
+
+from constants import BUTTON_SIZE
+from cursor import CursorWidget
+
+
+class BoomLabel(albow.controls.Label):
+
+    trim_line_top = 0
+
+    def __init__(self, text, width=None, **kwds):
+        albow.controls.Label.__init__(self, text, width, **kwds)
+        w, h = self.size
+        h -= self.trim_line_top * len(self.text.split('\n'))
+        self.size = (w, h)
+
+    def set_margin(self, margin):
+        """Add a set_margin method that recalculates the label size"""
+        old_margin = self.margin
+        w, h = self.size
+        d = margin - old_margin
+        self.margin = margin
+        self.size = (w + 2 * d, h + 2 * d)
+
+    def draw_all(self, surface):
+        bg_color = self.bg_color
+        self.bg_color = None
+        if bg_color is not None:
+            new_surface = surface.convert_alpha()
+            new_surface.fill(bg_color)
+            surface.blit(new_surface, surface.get_rect())
+        albow.controls.Label.draw_all(self, surface)
+        self._draw_all_no_bg(surface)
+        self.bg_color = bg_color
+
+    def _draw_all_no_bg(self, surface):
+        pass
+
+    def draw_with(self, surface, fg, _bg=None):
+        m = self.margin
+        align = self.align
+        width = surface.get_width()
+        y = m
+        lines = self.text.split("\n")
+        font = self.font
+        dy = font.get_linesize() - self.trim_line_top
+        for line in lines:
+            image = font.render(line, True, fg)
+            r = image.get_rect()
+            image = image.subsurface(r.clip(r.move(0, self.trim_line_top)))
+            r.top = y
+            if align == 'l':
+                r.left = m
+            elif align == 'r':
+                r.right = width - m
+            else:
+                r.centerx = width // 2
+            surface.blit(image, r)
+            y += dy
+
+
+class BoomButton(BoomLabel):
+
+    def __init__(self, text, action, screen):
+        super(BoomButton, self).__init__(text, font=get_font(20, 'Vera.ttf'),
+                margin=4)
+        self.bg_color = (0, 0, 0)
+        self._frame_color = Color(50, 50, 50)
+        self.action = action
+        self.screen = screen
+
+    def mouse_down(self, event):
+        self.action()
+        self.screen.state_widget.mouse_move(event)
+
+    def mouse_move(self, event):
+        self.screen.state.highlight_override = True
+
+    def draw(self, surface):
+        super(BoomButton, self).draw(surface)
+        r = surface.get_rect()
+        w = 2
+        top, bottom, left, right = r.top, r.bottom, r.left, r.right
+        draw_lines(surface, self._frame_color, False, [
+            (left, bottom), (left, top), (right - w, top), (right - w, bottom)
+            ], w)
+
+
+class MessageDialog(BoomLabel, CursorWidget):
+
+    def __init__(self, screen, text, wrap_width, style=None, **kwds):
+        CursorWidget.__init__(self, screen)
+        self.set_style(style)
+        paras = text.split("\n\n")
+        text = "\n".join([textwrap.fill(para, wrap_width) for para in paras])
+        BoomLabel.__init__(self, text, **kwds)
+
+    def set_style(self, style):
+        self.set_margin(5)
+        self.border_width = 1
+        self.border_color = (0, 0, 0)
+        self.bg_color = (127, 127, 127)
+        self.fg_color = (0, 0, 0)
+        if style == "JIM":
+            self.set(font=get_font(20, "Monospace.ttf"))
+            self.trim_line_top = 10
+            self.bg_color = Color(255, 175, 127, 191)
+            self.fg_color = (0, 0, 0)
+            self.border_color = (127, 15, 0)
+
+    def draw_all(self, surface):
+        root_surface = self.get_root().surface
+        overlay = root_surface.convert_alpha()
+        overlay.fill(Color(0, 0, 0, 191))
+        root_surface.blit(overlay, (0, 0))
+        BoomLabel.draw_all(self, surface)
+
+    def _draw_all_no_bg(self, surface):
+        CursorWidget.draw_all(self, surface)
+
+    def mouse_down(self, event):
+        self.dismiss()
+        self.screen.state_widget._mouse_move(mouse.get_pos())
+        for widget in self.screen.state_widget.subwidgets:
+            widget._mouse_move(mouse.get_pos())
+
+    def cursor_highlight(self):
+        return False
+
+
+class HandButton(albow.controls.Image):
+    """The fancy hand button for the widget"""
+
+    def __init__(self, action):
+        # FIXME: Yes, please.
+        this_image = get_image('items', 'hand.png')
+        albow.controls.Image.__init__(self, image=this_image)
+        self.action = action
+        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
+
+    def mouse_down(self, event):
+        self.action()
+
+
+class PopupMenuButton(albow.controls.Button):
+
+    def __init__(self, text, action):
+        albow.controls.Button.__init__(self, text, action)
+
+        self.font = get_font(16, 'Vera.ttf')
+        self.set_rect(Rect(0, 0, BUTTON_SIZE, BUTTON_SIZE))
+        self.margin = (BUTTON_SIZE - self.font.get_linesize()) / 2
+
+
+class PopupMenu(albow.menu.Menu, CursorWidget):
+
+    def __init__(self, screen):
+        CursorWidget.__init__(self, screen)
+        self.screen = screen
+        self.shell = screen.shell
+        items = [
+                ('Quit Game', 'quit'),
+                ('Exit to Main Menu', 'main_menu'),
+                ]
+        # albow.menu.Menu ignores title string
+        albow.menu.Menu.__init__(self, None, items)
+        self.font = get_font(16, 'Vera.ttf')
+
+    def show_menu(self):
+        """Call present, with the correct position"""
+        item_height = self.font.get_linesize()
+        menu_top = 600 - (len(self.items) * item_height + BUTTON_SIZE)
+        item = self.present(self.shell, (0, menu_top))
+        if item > -1:
+            # A menu item needs to be invoked
+            self.invoke_item(item)
+
+
+class BoomImageButton(albow.controls.Image):
+    """The fancy image button for the screens"""
+
+    FOLDER = None
+
+    def __init__(self, filename, x, y, action, enable=None):
+        this_image = get_image(self.FOLDER, filename)
+        albow.controls.Image.__init__(self, image=this_image)
+        self.action = action
+        self.set_rect(Rect((x, y), this_image.get_size()))
+        self.enable = enable
+
+    def draw(self, surface):
+        if self.is_enabled():
+            surface.blit(self.get_image(), self.get_rect())
+
+    def mouse_down(self, event):
+        if self.is_enabled():
+            self.action()
+
+    def is_enabled(self):
+        if self.enable:
+            return self.enable()
+        return True