ic
authorFrank DeMarco <frank.s.demarco@gmail.com>
Fri, 25 Apr 2014 17:22:01 +0000 (02:22 +0900)
committerFrank DeMarco <frank.s.demarco@gmail.com>
Fri, 25 Apr 2014 17:22:01 +0000 (02:22 +0900)
25 files changed:
.gitignore [new file with mode: 0644]
README [new file with mode: 0644]
config [new file with mode: 0644]
scale-sieve [new file with mode: 0755]
scale_sieve/ScaleSieve.py [new file with mode: 0644]
scale_sieve/__init__.py [new file with mode: 0644]
scale_sieve/pgfw/Animation.py [new file with mode: 0644]
scale_sieve/pgfw/Audio.py [new file with mode: 0644]
scale_sieve/pgfw/Configuration.py [new file with mode: 0644]
scale_sieve/pgfw/Delegate.py [new file with mode: 0644]
scale_sieve/pgfw/Display.py [new file with mode: 0644]
scale_sieve/pgfw/Game.py [new file with mode: 0644]
scale_sieve/pgfw/GameChild.py [new file with mode: 0644]
scale_sieve/pgfw/Input.py [new file with mode: 0644]
scale_sieve/pgfw/Interpolator.py [new file with mode: 0644]
scale_sieve/pgfw/Mainloop.py [new file with mode: 0644]
scale_sieve/pgfw/Profile.py [new file with mode: 0644]
scale_sieve/pgfw/ScreenGrabber.py [new file with mode: 0644]
scale_sieve/pgfw/Setup.py [new file with mode: 0644]
scale_sieve/pgfw/SetupWin.py [new file with mode: 0644]
scale_sieve/pgfw/Sprite.py [new file with mode: 0644]
scale_sieve/pgfw/TimeFilter.py [new file with mode: 0644]
scale_sieve/pgfw/Vector.py [new file with mode: 0644]
scale_sieve/pgfw/VideoRecorder.py [new file with mode: 0644]
scale_sieve/pgfw/__init__.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..75ba69a
--- /dev/null
+++ b/README
@@ -0,0 +1,18 @@
+-----------
+Scale Sieve
+-----------
+
+Split the triangles without touching them.  Go up to make the
+triangles come faster.
+
+Requirements
+------------
+
+Windows ---- none
+Linux/Mac -- Python 2.6+, Pygame 1.8.1+
+
+Running
+-------
+
+Windows ---- double-click "scale-sieve" EXE
+Linux/Mac -- run "./scale-sieve" on the command line
diff --git a/config b/config
new file mode 100644 (file)
index 0000000..58fdae7
--- /dev/null
+++ b/config
@@ -0,0 +1,8 @@
+[display]
+dimensions = 420, 700
+
+[keys]
+quit = K_ESCAPE
+
+[interpolate]
+scale = C 0 10, 680 20, 1000 40
diff --git a/scale-sieve b/scale-sieve
new file mode 100755 (executable)
index 0000000..250d984
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+
+from os import environ, execvp, chdir, getcwd
+from os.path import exists, join, dirname
+from sys import version_info, argv
+
+def can_import(module_name):
+    try:
+        __import__(module_name)
+    except ImportError:
+        return False
+    else:
+        return True
+
+def is_python_3():
+    return version_info[0] >= 3
+
+def is_current_version(file_name):
+    version = map(int, file_name.replace("python", "").split("."))
+    return version == list(version_info)[:2]
+
+def launch_alternative(alternatives):
+    for alternative in alternatives:
+        if not is_current_version(alternative):
+            for root in environ["PATH"].split(":"):
+                if exists(join(root, alternative)):
+                    execvp(alternative, [alternative] + argv)
+
+def move_to_executable():
+    chdir(dirname(argv[0]))
+
+if is_python_3():
+    launch_alternative(["python2", "python2.7", "python2.6"])
+
+if not can_import("pygame"):
+    launch_alternative(["python2.7", "python2.6"])
+
+if "--go-to-dir" in argv:
+    move_to_executable()
+
+from scale_sieve.ScaleSieve import ScaleSieve
+
+ScaleSieve().run()
diff --git a/scale_sieve/ScaleSieve.py b/scale_sieve/ScaleSieve.py
new file mode 100644 (file)
index 0000000..cb0c3b3
--- /dev/null
@@ -0,0 +1,57 @@
+from pygame import Surface, PixelArray
+
+from scale_sieve.pgfw.Game import Game
+from scale_sieve.pgfw.GameChild import GameChild
+from scale_sieve.pgfw.Sprite import Sprite
+
+class ScaleSieve(Game):
+
+    def __init__(self):
+        Game.__init__(self)
+        self.background = Surface(self.display.screen.get_size())
+        self.background.fill((255, 80, 190))
+
+    def set_children(self):
+        Game.set_children(self)
+        self.sieve = Sieve(self)
+
+    def update(self):
+        self.display.screen.blit(self.background, (0, 0))
+        self.sieve.update()
+
+
+class Strip(Sprite):
+
+    def __init__(self, parent):
+        Sprite.__init__(self, parent)
+        self.display_surface = self.get_display_surface()
+        self.add_frames()
+
+    def add_frames(self):
+        pass
+
+
+class Sieve(Strip):
+
+    def __init__(self, parent):
+        Strip.__init__(self, parent)
+        self.location.center = self.display_surface.get_rect().center
+
+    def add_frames(self):
+        surface = Surface((1000, 30))
+        transparent_color = (255, 0, 255)
+        surface.fill(transparent_color)
+        surface.set_colorkey(transparent_color)
+        frames = surface, surface.copy()
+        background_colors = (255, 255, 0), (0, 0, 213)
+        divider_colors = (0, 255, 0), (153, 0, 204)
+        pixel_arrays = PixelArray(frames[0]), PixelArray(frames[1])
+        bw = 5
+        for x in xrange(len(pixel_arrays[0])):
+            for y in xrange(bw, len(pixel_arrays[0][0]) - bw):
+                pixel_arrays[0][x][y] = background_colors[(y + x) % 2]
+                pixel_arrays[1][x][y] = background_colors[(y + x + 1) % 2]
+        for pixels in pixel_arrays:
+            del pixels
+        for frame in frames:
+            self.add_frame(frame)
diff --git a/scale_sieve/__init__.py b/scale_sieve/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/scale_sieve/pgfw/Animation.py b/scale_sieve/pgfw/Animation.py
new file mode 100644 (file)
index 0000000..bd21ebb
--- /dev/null
@@ -0,0 +1,134 @@
+from GameChild import GameChild
+
+class Animation(GameChild):
+
+    def __init__(self, parent, method=None, interval=None, unfiltered=False):
+        GameChild.__init__(self, parent)
+        self.unfiltered = unfiltered
+        self.default_method = method or self.build_frame
+        self.accounts = {}
+        self.register(self.default_method, interval=interval)
+        self.current_elapsed = 0
+        self.last_update = 0
+
+    def build_frame(self):
+        pass
+
+    def register(self, *args, **kwargs):
+        interval = None
+        if kwargs.has_key("interval"):
+            interval = kwargs["interval"]
+        for method in args:
+            if method not in self.accounts:
+                self.accounts[method] = Account(interval, self)
+            else:
+                self.accounts[method].set_interval(interval)
+
+    def play(self, method=None, interval=None, delay=0, play_once=False,
+             **kwargs):
+        account = self.accounts[self.get_default(method)]
+        account.set_delay(delay)
+        account.set_args(kwargs)
+        account.set_play_once(play_once)
+        if interval:
+            account.set_interval(interval)
+        account.play()
+
+    def get_default(self, method):
+        if not method:
+            method = self.default_method
+        return method
+
+    def halt(self, method=None):
+        if not method:
+            for account in self.accounts.values():
+                account.halt()
+        else:
+            if self.accounts.has_key(method):
+                self.accounts[method].halt()
+
+    def is_playing(self, method=None, check_all=False, include_delay=False):
+        if check_all:
+            return any(self.is_account_playing(account, include_delay) for \
+                       method, account in self.accounts.iteritems())
+        return self.is_account_playing(self.accounts[self.get_default(method)],
+                                       include_delay)
+
+    def is_account_playing(self, account, include_delay):
+        return account.playing and (not include_delay or not account.delay)
+
+    def update(self):
+        for method, account in self.accounts.iteritems():
+            if account.update():
+                method(**account.args)
+
+
+class Account:
+
+    def __init__(self, interval, animation):
+        self.animation = animation
+        self.time_filter = animation.get_game().time_filter
+        self.set_interval(interval)
+        self.set_delay(0)
+        self.set_play_once(False)
+        self.interval_index = 0
+        self.last_frame = 0
+        self.halt()
+
+    def set_interval(self, interval):
+        if isinstance(interval, int) or isinstance(interval, str):
+            interval = [interval]
+        self.interval = interval
+
+    def set_delay(self, delay):
+        self.delay = delay
+
+    def set_play_once(self, play_once):
+        self.play_once = play_once
+
+    def set_args(self, args):
+        self.args = args
+
+    def play(self):
+        self.playing = True
+
+    def halt(self):
+        self.last_update = None
+        self.playing = False
+
+    def update(self):
+        if self.playing:
+            if self.animation.unfiltered:
+                ticks = self.time_filter.get_unfiltered_ticks()
+            else:
+                ticks = self.time_filter.get_ticks()
+            self.update_delay(ticks)
+            if not self.delay:
+                interval = self.interval
+                if interval:
+                    if ticks - self.last_frame < self.get_current_interval():
+                        return False
+                    self.last_frame = ticks
+                    self.increment_interval_index()
+                if self.play_once:
+                    self.halt()
+                return True
+
+    def get_current_interval(self):
+        return self.interval[self.interval_index]
+
+    def increment_interval_index(self):
+        index = self.interval_index + 1
+        if index >= len(self.interval):
+            index = 0
+        self.interval_index = index
+
+    def update_delay(self, ticks):
+        delay = self.delay
+        if delay > 0:
+            last_update = self.last_update or ticks
+            delay -= ticks - last_update
+            if delay < 0:
+                delay = 0
+        self.last_update = ticks
+        self.delay = delay
diff --git a/scale_sieve/pgfw/Audio.py b/scale_sieve/pgfw/Audio.py
new file mode 100644 (file)
index 0000000..2c230e9
--- /dev/null
@@ -0,0 +1,92 @@
+from os import listdir
+from os.path import join
+
+from pygame.mixer import Channel, Sound, music, find_channel
+
+from GameChild import *
+from Input import *
+
+class Audio(GameChild):
+
+    current_channel = None
+    paused = False
+    muted = False
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_fx()
+        self.subscribe(self.respond)
+
+    def load_fx(self):
+        fx = {}
+        if self.get_configuration().has_option("audio", "sfx-path"):
+            root = self.get_resource("audio", "sfx-path")
+            if root:
+                for name in listdir(root):
+                    fx[name.split(".")[0]] = Sound(join(root, name))
+        self.fx = fx
+
+    def respond(self, event):
+        if self.delegate.compare(event, "mute"):
+            self.mute()
+
+    def mute(self):
+        self.muted = True
+        self.set_volume()
+
+    def unmute(self):
+        self.muted = False
+        self.set_volume()
+
+    def set_volume(self):
+        volume = int(not self.muted)
+        music.set_volume(volume)
+        if self.current_channel:
+            self.current_channel.set_volume(volume)
+
+    def play_bgm(self, path, stream=False):
+        self.stop_current_channel()
+        if stream:
+            music.load(path)
+            music.play(-1)
+        else:
+            self.current_channel = Sound(path).play(-1)
+        self.set_volume()
+
+    def stop_current_channel(self):
+        music.stop()
+        if self.current_channel:
+            self.current_channel.stop()
+        self.current_channel = None
+        self.paused = False
+
+    def play_fx(self, name, panning=.5):
+        if not self.muted:
+            channel = find_channel(True)
+            if panning != .5:
+                offset = 1 - abs(panning - .5) * 2
+                if panning < .5:
+                    channel.set_volume(1, offset)
+                else:
+                    channel.set_volume(offset, 1)
+            channel.play(self.fx[name])
+
+    def pause(self):
+        channel = self.current_channel
+        paused = self.paused
+        if paused:
+            music.unpause()
+            if channel:
+                channel.unpause()
+        else:
+            music.pause()
+            if channel:
+                channel.pause()
+        self.paused = not paused
+
+    def is_bgm_playing(self):
+        current = self.current_channel
+        if current and current.get_sound():
+            return True
+        return music.get_busy()
diff --git a/scale_sieve/pgfw/Configuration.py b/scale_sieve/pgfw/Configuration.py
new file mode 100644 (file)
index 0000000..7e00a3b
--- /dev/null
@@ -0,0 +1,442 @@
+from os import sep, getcwd
+from os.path import join, exists, basename, dirname, expanduser
+from sys import argv
+from re import match
+from pprint import pformat
+
+from ConfigParser import RawConfigParser
+
+class Configuration(RawConfigParser):
+
+    default_project_file_rel_path = "config"
+    default_resource_paths = [".", "resource"]
+
+    def __init__(self, project_file_rel_path=None, resource_path=None,
+                 type_declarations=None):
+        RawConfigParser.__init__(self)
+        self.project_file_rel_path = project_file_rel_path
+        self.resource_path = resource_path
+        self.modifiable = {}
+        self.order = []
+        self.set_type_declarations(type_declarations)
+        self.set_defaults()
+        self.read_project_config_file()
+        self.modify_defaults()
+        self.print_debug(self)
+
+    def set_type_declarations(self, type_declarations):
+        if type_declarations is None:
+            type_declarations = TypeDeclarations()
+        self.type_declarations = type_declarations
+
+    def translate_path(self, path):
+        new = ""
+        if path and path[0] == sep:
+            new += sep
+        return expanduser("{0}{1}".format(new, join(*path.split(sep))))
+
+    def set_defaults(self):
+        add_section = self.add_section
+        set_option = self.set
+        section = "setup"
+        add_section(section)
+        set_option(section, "package-root", basename(getcwd()), False)
+        set_option(section, "additional-packages", "", False)
+        set_option(section, "title", "", False)
+        set_option(section, "classifiers", "", False)
+        set_option(section, "resource-search-path", "./, resource/", False)
+        set_option(section, "installation-dir", "/usr/local/share/games/",
+                   False)
+        set_option(section, "changelog", "changelog", False)
+        set_option(section, "description-file", "", False)
+        set_option(section, "init-script", "", False)
+        set_option(section, "version", "", False)
+        set_option(section, "summary", "", False)
+        set_option(section, "license", "", False)
+        set_option(section, "platforms", "", False)
+        set_option(section, "contact-name", "", False)
+        set_option(section, "contact-email", "", False)
+        set_option(section, "url", "", False)
+        set_option(section, "requirements", "", False)
+        set_option(section, "main-object", "pgfw/Game.py", False)
+        set_option(section, "resource-path-identifier", "resource_path", False)
+        set_option(section, "special-char-placeholder", "_", False)
+        set_option(section, "whitespace-placeholder", "-", False)
+        set_option(section, "windows-dist-path", "dist/win/", False)
+        set_option(section, "windows-icon-path", "", False)
+        set_option(section, "lowercase-boolean-true", "yes", False)
+        section = "display"
+        add_section(section)
+        set_option(section, "dimensions", "480, 360", False)
+        set_option(section, "frame-duration", "40", False)
+        set_option(section, "wait-duration", "2", False)
+        set_option(section, "caption", "", False)
+        set_option(section, "centered", "yes", False)
+        set_option(section, "icon-path", "", False)
+        set_option(section, "skip-frames", "no", False)
+        set_option(section, "fullscreen", "no", False)
+        set_option(section, "windowed-flag", "wi", False)
+        set_option(section, "show-framerate", "no", False)
+        set_option(section, "framerate-display-flag", "fr", False)
+        set_option(section, "framerate-text-size", "16", False)
+        set_option(section, "framerate-text-color", "0, 0, 0", False)
+        set_option(section, "framerate-text-background", "255, 255, 255", False)
+        section = "input"
+        add_section(section)
+        set_option(section, "release-suffix", "-release", False)
+        section = "sprite"
+        add_section(section)
+        set_option(section, "transparent-color", "magenta", False)
+        section = "screen-captures"
+        add_section(section)
+        set_option(section, "rel-path", "caps", False)
+        set_option(section, "file-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        section = "video-recordings"
+        add_section(section)
+        set_option(section, "rel-path", "vids", False)
+        set_option(section, "directory-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        set_option(section, "frame-format", "RGB", False)
+        set_option(section, "framerate", "100", False)
+        section = "mouse"
+        add_section(section)
+        set_option(section, "visible", "yes", False)
+        set_option(section, "double-click-time-limit", ".5", False)
+        section = "keys"
+        add_section(section)
+        set_option(section, "up", "K_UP, K_w", False)
+        set_option(section, "right", "K_RIGHT, K_d", False)
+        set_option(section, "down", "K_DOWN, K_s", False)
+        set_option(section, "left", "K_LEFT, K_a", False)
+        set_option(section, "capture-screen", "K_F9", False)
+        set_option(section, "toggle-fullscreen", "K_F11", False)
+        set_option(section, "reset-game", "K_F8", False)
+        set_option(section, "record-video", "K_F10", False)
+        set_option(section, "mute", "K_F12", False)
+        set_option(section, "toggle-interpolator", "K_F7", False)
+        section = "joy"
+        add_section(section)
+        set_option(section, "advance", "7", False)
+        set_option(section, "pause", "7", False)
+        set_option(section, "select", "6", False)
+        section = "event"
+        add_section(section)
+        set_option(section, "user-event-id", "USEREVENT", False)
+        set_option(section, "command-id-offset", "1", False)
+        set_option(section, "command-key", "command", False)
+        set_option(section, "cancel-flag-key", "cancel", False)
+        section = "audio"
+        add_section(section)
+        set_option(section, "sfx-path", "aud/fx/", False)
+        section = "interpolator-gui"
+        add_section(section)
+        set_option(section, "margin", "80", False)
+        set_option(section, "marker-color", "255, 0, 0", False)
+        set_option(section, "marker-size", "11", False)
+        set_option(section, "curve-color", "0, 255, 0", False)
+        set_option(section, "label-size", "16", False)
+        set_option(section, "label-precision", "2", False)
+        set_option(section, "axis-label-count", "8", False)
+        set_option(section, "prompt-size", "380, 60", False)
+        set_option(section, "prompt-border-color", "255, 0, 0", False)
+        set_option(section, "prompt-border-width", "3", False)
+        set_option(section, "prompt-character-limit", "21", False)
+        set_option(section, "prompt-text-size", "42", False)
+        set_option(section, "template-nodeset", "L 0 0, 1000 1", False)
+        set_option(section, "template-nodeset-name", "template", False)
+        set_option(section, "flat-y-range", "1", False)
+
+    def add_section(self, name):
+        if name not in self.order:
+            self.order.append(name)
+        RawConfigParser.add_section(self, name)
+
+    def set(self, section, option, value, modifiable=True):
+        if modifiable:
+            if section not in self.order:
+                self.order.append(section)
+            if section not in self.modifiable:
+                self.modifiable[section] = []
+            if option not in self.modifiable[section]:
+                self.modifiable[section].append(option)
+        RawConfigParser.set(self, section, option, value)
+
+    def read_project_config_file(self):
+        path = self.locate_project_config_file()
+        if path:
+            fp = open(path)
+            self.set_modifiable(fp)
+            fp.seek(0)
+            self.readfp(fp)
+            fp.seek(0)
+            self.set_order(fp)
+            fp.close()
+        else:
+            self.print_debug("No configuration file found")
+
+    def locate_project_config_file(self):
+        rel_path = self.project_file_rel_path
+        if not rel_path:
+            rel_path = self.default_project_file_rel_path
+        if exists(rel_path) and not self.is_shared_mode():
+            return rel_path
+        if self.resource_path:
+            installed_path = join(self.resource_path, rel_path)
+            if exists(installed_path):
+                return installed_path
+
+    def set_order(self, fp):
+        self.order = order = []
+        for line in file(self.locate_project_config_file()):
+            result = match("^\s*\[(.*)\]\s*$", line)
+            if result:
+                order.append(result.group(1))
+
+    def set_modifiable(self, fp):
+        config = RawConfigParser()
+        config.readfp(fp)
+        modifiable = self.modifiable
+        for section in config._sections:
+            if section not in modifiable:
+                modifiable[section] = []
+            for option in config._sections[section]:
+                if option != "__name__" and option not in modifiable[section]:
+                    modifiable[section].append(option)
+
+    def is_shared_mode(self):
+        return "-s" in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print statement
+
+    def is_debug_mode(self):
+        return "-d" in argv
+
+    def modify_defaults(self):
+        self.set_installation_path()
+        self.set_resource_search_path()
+        self.set_screen_captures_path()
+        self.set_video_recordings_path()
+        self.set_data_exclusion_list()
+        self.set_requirements()
+
+    def set_installation_path(self):
+        self.set("setup", "installation-path",
+                 join(self.get("setup", "installation-dir"),
+                      self.get("setup", "package-root")), False)
+
+    def set_resource_search_path(self):
+        section, option = "setup", "resource-search-path"
+        search_path = self.get(section, option)
+        if self.resource_path:
+            search_path.append(self.resource_path)
+        else:
+            search_path.append(self.get("setup", "installation-path"))
+        self.set(section, option, search_path, False)
+
+    def get(self, section, option):
+        value = RawConfigParser.get(self, section, option)
+        if value is None:
+            value = self.get_substitute(section, option)
+        return self.cast_value(section, option, value)
+
+    def get_substitute(self, section, option):
+        if section == "display":
+            if option == "caption":
+                return self.get("setup", "title")
+
+    def cast_value(self, section, option, value):
+        pair = section, option
+        types = self.type_declarations
+        if type(value) == str:
+            if pair in types["bool"]:
+                if value.lower() == self.get("setup", "lowercase-boolean-true"):
+                    return True
+                return False
+            elif pair in types["int"]:
+                return int(value)
+            elif pair in types["float"]:
+                return float(value)
+            elif pair in types["path"]:
+                return self.translate_path(value)
+            elif pair in types["list"]:
+                if value == "":
+                    return []
+                else:
+                    return map(str.strip, value.split(types.list_member_sep))
+            elif pair in types["int-list"]:
+                return map(int, value.split(types.list_member_sep))
+            elif pair in types["float-list"]:
+                return map(float, value.split(types.list_member_sep))
+        return value
+
+    def set_screen_captures_path(self):
+        section, option = "screen-captures", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def build_home_path(self):
+        return join("~", "." + self.get("setup", "package-root"))
+
+    def set_video_recordings_path(self):
+        section, option = "video-recordings", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def set_data_exclusion_list(self):
+        section, option = "setup", "data-exclude"
+        exclude = []
+        if self.has_option(section, option):
+            exclude = self.get(section, option)
+        exclude += [".git", ".gitignore", "README", "build/", "dist/",
+                    "setup.py", "MANIFEST", "PKG-INFO",
+                    self.get("setup", "changelog"),
+                    self.get("setup", "package-root")]
+        for location in self.get("setup", "additional-packages"):
+            exclude.append(location)
+        self.set(section, option, exclude, False)
+
+    def set_requirements(self):
+        section, option = "setup", "requirements"
+        requirements = []
+        if self.has_option(section, option):
+            requirements = self.get(section, option)
+        if "pygame" not in requirements:
+            requirements.append("pygame")
+        self.set(section, option, requirements, False)
+
+    def get_section(self, section):
+        assignments = {}
+        for option in self.options(section):
+            assignments[option] = self.get(section, option)
+        return assignments
+
+    def __repr__(self):
+        config = {}
+        for section in self.sections():
+            config[section] = self.get_section(section)
+        return pformat(config, 2, 1)
+
+    def items(self, section):
+        items = []
+        for option in self.options(section):
+            items.append((option, self.get(section, option)))
+        return items
+
+    def write(self, fp=None):
+        modifiable = self.modifiable
+        use_main = fp is None
+        if use_main:
+            path = self.locate_project_config_file()
+            if not path:
+                path = join(self.resource_path or "",
+                            self.default_project_file_rel_path)
+            fp = open(path, "w")
+        break_line = False
+        for section in self.order:
+            if section in modifiable:
+                break_line and fp.write("\n")
+                fp.write("[%s]\n" % section)
+                for option in modifiable[section]:
+                    if self.has_option(section, option):
+                        value = self.get(section, option)
+                        fp.write("%s = %s\n" % (option,
+                                                self.get_raw_value(value)))
+                break_line = True
+        if use_main:
+            fp.close()
+
+    def get_raw_value(self, value):
+        if isinstance(value, list):
+            raw = ""
+            for ii, value in enumerate(value):
+                if ii:
+                    raw += ", "
+                raw += str(value)
+        else:
+            raw = str(value)
+        return raw
+
+    def clear_section(self, section):
+        if self.has_section(section):
+            for option in self.options(section):
+                self.remove_option(section, option)
+
+
+class TypeDeclarations(dict):
+
+    list_member_sep = ','
+
+    defaults = {
+
+        "display": {"int": ["frame-duration", "wait-duration",
+                            "framerate-text-size"],
+
+                    "bool": ["centered", "skip-frames", "fullscreen",
+                             "show-framerate"],
+
+                    "int-list": ["dimensions", "framerate-text-color",
+                                 "framerate-text-background"]},
+
+        "screen-captures": {"path": ["rel-path", "path"]},
+
+        "video-recordings": {"path": ["rel-path", "path"],
+
+                             "int": "framerate"},
+
+        "setup": {"list": ["classifiers", "resource-search-path",
+                           "requirements", "data-exclude",
+                           "additional-packages"],
+
+                  "path": ["installation-dir", "changelog", "description-file",
+                           "main-object", "icon-path", "windows-dist-path",
+                           "package-root"]},
+
+        "mouse": {"float": "double-click-time-limit",
+
+                  "bool": "visible"},
+
+        "keys": {"list": ["up", "right", "down", "left"]},
+
+        "joy": {"int": ["advance", "pause", "select"]},
+
+        "audio": {"path": "sfx-path"},
+
+        "event": {"int": "command-id-offset"},
+
+        "interpolator-gui": {"int": ["margin", "marker-size", "label-size",
+                                     "axis-label-count", "label-precision",
+                                     "prompt-border-width",
+                                     "prompt-character-limit",
+                                     "prompt-text-size", "flat-y-range"],
+
+                             "int-list": ["marker-color", "curve-color",
+                                          "prompt-size",
+                                          "prompt-border-color"]},
+
+        }
+
+    additional_defaults = {}
+
+    def __init__(self):
+        dict.__init__(self, {"bool": [], "int": [], "float": [], "path": [],
+                             "list": [], "int-list": [], "float-list": []})
+        self.add_chart(self.defaults)
+        self.add_chart(self.additional_defaults)
+
+    def add(self, cast, section, option):
+        self[cast].append((section, option))
+
+    def add_chart(self, chart):
+        for section, declarations in chart.iteritems():
+            for cast, options in declarations.iteritems():
+                if type(options) != list:
+                    options = [options]
+                for option in options:
+                    self.add(cast, section, option)
diff --git a/scale_sieve/pgfw/Delegate.py b/scale_sieve/pgfw/Delegate.py
new file mode 100644 (file)
index 0000000..ce62c1b
--- /dev/null
@@ -0,0 +1,87 @@
+from pygame.event import get, pump, Event, post
+from pygame.locals import *
+
+from GameChild import GameChild
+from Input import Input
+
+class Delegate(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.subscribers = dict()
+        self.load_configuration()
+        self.disable()
+
+    def load_configuration(self):
+        config = self.get_configuration("event")
+        self.cancel_flag_key = config["cancel-flag-key"]
+        self.command_key = config["command-key"]
+        self.command_event_id = config["command-id-offset"] + \
+                                globals()[config["user-event-id"]]
+
+    def disable(self):
+        self.enabled = False
+
+    def enable(self):
+        self.enabled = True
+        self.interpolator = self.get_game().interpolator
+
+    def dispatch(self):
+        if self.enabled:
+            subscribers = self.subscribers
+            for evt in get():
+                kind = evt.type
+                if kind in subscribers:
+                    for subscriber in subscribers[kind]:
+                        if not self.interpolator.is_gui_active() or \
+                               hasattr(subscriber, "im_class") and \
+                               (subscriber.im_class == Input or \
+                                subscriber.im_class == \
+                                self.interpolator.gui.__class__):
+                            self.print_debug("Passing %s to %s" % (evt,
+                                                                   subscriber))
+                            subscriber(evt)
+        else:
+            pump()
+
+    def add_subscriber(self, callback, kind=None):
+        self.print_debug("Subscribing %s to %s" % (callback, kind))
+        if kind is None:
+            kind = self.command_event_id
+        subscribers = self.subscribers
+        if kind not in subscribers:
+            subscribers[kind] = list()
+        subscribers[kind].append(callback)
+
+    def is_command(self, event):
+        return event.type == self.command_event_id
+
+    def remove_subscriber(self, callback, kind=None):
+        if kind is None:
+            kind = self.command_event_id
+        self.subscribers[kind].remove(callback)
+
+    def compare(self, evt, commands=None, cancel=False, **attributes):
+        if evt.type == self.command_event_id:
+            self.add_cancel_flag_to_attributes(attributes, cancel)
+        if commands is not None and not isinstance(commands, list):
+            commands = [commands]
+        if commands is not None:
+            if not self.command_in_list(evt, commands):
+                return False
+        return all(key in evt.dict and evt.dict[key] == value for \
+                   key, value in attributes.iteritems())
+
+    def add_cancel_flag_to_attributes(self, attributes, cancel):
+        attributes[self.cancel_flag_key] = cancel
+
+    def command_in_list(self, evt, commands):
+        return self.get_command_attribute(evt) in commands
+
+    def get_command_attribute(self, evt):
+        return evt.dict[self.command_key]
+
+    def post(self, command=None, cancel=False, **attributes):
+        attributes[self.command_key] = command
+        self.add_cancel_flag_to_attributes(attributes, cancel)
+        post(Event(self.command_event_id, attributes))
diff --git a/scale_sieve/pgfw/Display.py b/scale_sieve/pgfw/Display.py
new file mode 100644 (file)
index 0000000..c9e4b52
--- /dev/null
@@ -0,0 +1,81 @@
+from os import environ
+
+from pygame import display, image, mouse
+from pygame.locals import *
+
+from GameChild import *
+
+class Display(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.align_window()
+        self.init_screen()
+        self.set_caption()
+        self.set_icon()
+        self.set_mouse_visibility()
+        self.subscribe(self.toggle_fullscreen)
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.centered = config["centered"]
+        self.fullscreen_enabled = config["fullscreen"]
+        self.caption = config["caption"]
+        self.windowed_flag = config["windowed-flag"]
+        self.icon_path = self.get_resource("display", "icon-path")
+        self.mouse_visibility = self.get_configuration("mouse", "visible")
+
+    def align_window(self):
+        if self.centered:
+            environ["SDL_VIDEO_CENTERED"] = "1"
+
+    def init_screen(self):
+        flags = 0
+        if self.fullscreen_requested():
+            flags = FULLSCREEN
+        self.set_screen(flags)
+
+    def fullscreen_requested(self):
+        return not self.check_command_line(self.windowed_flag) and \
+               self.fullscreen_enabled
+
+    def set_screen(self, flags=0, dimensions=None):
+        self.dimensions_changed = dimensions is not None
+        if dimensions is None:
+            if display.get_surface():
+                dimensions = display.get_surface().get_size()
+            else:
+                dimensions = self.get_configuration("display", "dimensions")
+        self.screen = display.set_mode(dimensions, flags)
+        if self.dimensions_changed:
+            interpolator = self.get_game().interpolator
+            if interpolator.gui_enabled:
+                interpolator.gui.rearrange()
+
+    def set_caption(self):
+        display.set_caption(self.caption)
+
+    def set_icon(self):
+        if self.icon_path:
+            print self.icon_path
+            display.set_icon(image.load(self.icon_path).convert_alpha())
+
+    def set_mouse_visibility(self, visibility=None):
+        if visibility is None:
+            visibility = self.mouse_visibility
+        return mouse.set_visible(visibility)
+
+    def get_screen(self):
+        return self.screen
+
+    def get_size(self):
+        return self.screen.get_size()
+
+    def toggle_fullscreen(self, event):
+        if self.delegate.compare(event, "toggle-fullscreen"):
+            screen = self.screen
+            cpy = screen.convert()
+            self.set_screen(self.screen.get_flags() ^ FULLSCREEN)
+            screen.blit(cpy, (0, 0))
diff --git a/scale_sieve/pgfw/Game.py b/scale_sieve/pgfw/Game.py
new file mode 100644 (file)
index 0000000..47d47f6
--- /dev/null
@@ -0,0 +1,74 @@
+import pygame
+from pygame.locals import *
+
+from GameChild import GameChild
+from Mainloop import Mainloop
+from Audio import Audio
+from Display import Display
+from Configuration import Configuration
+from Delegate import Delegate
+from Input import Input
+from ScreenGrabber import ScreenGrabber
+from Profile import Profile
+from VideoRecorder import VideoRecorder
+from Interpolator import Interpolator
+from TimeFilter import TimeFilter
+
+class Game(GameChild):
+
+    resource_path = None
+
+    def __init__(self, config_rel_path=None, type_declarations=None):
+        self.profile = Profile(self)
+        GameChild.__init__(self)
+        self.print_debug(pygame.version.ver)
+        self.config_rel_path = config_rel_path
+        self.type_declarations = type_declarations
+        self.set_configuration()
+        pygame.init()
+        self.set_children()
+        self.subscribe(self.end, QUIT)
+        self.subscribe(self.end)
+        self.delegate.enable()
+
+    def set_configuration(self):
+        self.configuration = Configuration(self.config_rel_path,
+                                           self.resource_path,
+                                           self.type_declarations)
+
+    def set_children(self):
+        self.time_filter = TimeFilter(self)
+        self.delegate = Delegate(self)
+        self.display = Display(self)
+        self.mainloop = Mainloop(self)
+        self.input = Input(self)
+        self.audio = Audio(self)
+        self.screen_grabber = ScreenGrabber(self)
+        self.video_recorder = VideoRecorder(self)
+        self.interpolator = Interpolator(self)
+
+    def frame(self):
+        self.time_filter.update()
+        self.delegate.dispatch()
+        if not self.interpolator.is_gui_active():
+            self.update()
+        else:
+            self.interpolator.gui.update()
+        self.video_recorder.update()
+
+    def run(self):
+        self.mainloop.run()
+
+    def update(self):
+        pass
+
+    def blit(self, source, destination, area=None, special_flags=0):
+        self.get_screen().blit(source, destination, area, special_flags)
+
+    def get_rect(self):
+        return self.get_screen().get_rect()
+
+    def end(self, evt):
+        if evt.type == QUIT or self.delegate.compare(evt, "quit"):
+            self.mainloop.stop()
+            self.profile.end()
diff --git a/scale_sieve/pgfw/GameChild.py b/scale_sieve/pgfw/GameChild.py
new file mode 100644 (file)
index 0000000..8808dce
--- /dev/null
@@ -0,0 +1,85 @@
+from os.path import exists, join, basename, normpath, abspath
+from sys import argv
+
+from pygame import mixer, event, time
+from pygame.locals import *
+
+import Game
+
+class GameChild:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.game = self.get_game()
+
+    def get_game(self):
+        current = self
+        while not isinstance(current, Game.Game):
+            current = current.parent
+        return current
+
+    def get_configuration(self, section=None, option=None):
+        config = self.game.configuration
+        if option and section:
+            return config.get(section, option)
+        if section:
+            return config.get_section(section)
+        return config
+
+    def get_input(self):
+        return self.game.input
+
+    def get_screen(self):
+        return self.game.display.get_screen()
+
+    def get_display_surface(self):
+        current = self
+        attribute = "display_surface"
+        while not isinstance(current, Game.Game):
+            if hasattr(current, attribute):
+                return getattr(current, attribute)
+            current = current.parent
+        return current.display.get_screen()
+
+    def get_audio(self):
+        return self.game.audio
+
+    def get_delegate(self):
+        return self.game.delegate
+
+    def get_resource(self, path_or_section, option=None):
+        config = self.get_configuration()
+        rel_path = path_or_section
+        if option is not None:
+            rel_path = config.get(path_or_section, option)
+        if rel_path:
+            for root in config.get("setup", "resource-search-path"):
+                if self.is_shared_mode() and not self.is_absolute_path(root):
+                    continue
+                path = join(root, rel_path)
+                if exists(path):
+                    return path
+        self.print_debug("Couldn't find resource: {0} {1}".\
+                                   format(path_or_section, option))
+
+    def is_shared_mode(self):
+        return self.check_command_line("s")
+
+    def check_command_line(self, flag):
+        return "-" + flag in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print statement
+
+    def is_debug_mode(self):
+        return self.check_command_line("d")
+
+    def is_absolute_path(self, path):
+        return normpath(path) == abspath(path)
+
+    def subscribe(self, callback, kind=None):
+        self.game.delegate.add_subscriber(callback, kind)
+
+    def unsubscribe(self, callback, kind=None):
+        self.game.delegate.remove_subscriber(callback, kind)
diff --git a/scale_sieve/pgfw/Input.py b/scale_sieve/pgfw/Input.py
new file mode 100644 (file)
index 0000000..6f888bf
--- /dev/null
@@ -0,0 +1,204 @@
+from time import time as get_secs
+
+from pygame import joystick as joy
+from pygame.key import get_pressed
+from pygame.locals import *
+
+from GameChild import *
+
+class Input(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.last_mouse_down_left = None
+        self.joystick = Joystick()
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.set_any_press_ignore_list()
+        self.unsuppress()
+        self.subscribe_to_events()
+        self.build_key_map()
+        self.build_joy_button_map()
+
+    def load_configuration(self):
+        self.release_suffix = self.get_configuration("input", "release-suffix")
+        self.key_commands = self.get_configuration().items("keys")
+        self.double_click_time_limit = self.get_configuration(
+            "mouse", "double-click-time-limit")
+
+    def set_any_press_ignore_list(self):
+        self.any_press_ignored = set(["capture-screen", "toggle-fullscreen",
+                                      "reset-game", "record-video", "quit",
+                                      "mute", "toggle-interpolator"])
+        self.any_press_ignored_keys = set()
+
+    def unsuppress(self):
+        self.suppressed = False
+
+    def subscribe_to_events(self):
+        self.subscribe(self.translate_key, KEYDOWN)
+        self.subscribe(self.translate_key, KEYUP)
+        self.subscribe(self.translate_joy_button, JOYBUTTONDOWN)
+        self.subscribe(self.translate_joy_button, JOYBUTTONUP)
+        self.subscribe(self.translate_axis_motion, JOYAXISMOTION)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONDOWN)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONUP)
+
+    def build_key_map(self):
+        key_map = {}
+        for command, keys in self.key_commands:
+            key_map[command] = []
+            if type(keys) == str:
+                keys = [keys]
+            for key in keys:
+                key_map[command].append(globals()[key])
+        self.key_map = key_map
+
+    def build_joy_button_map(self):
+        self.joy_button_map = self.get_configuration("joy")
+
+    def suppress(self):
+        self.suppressed = True
+
+    def translate_key(self, event):
+        if not self.suppressed:
+            cancel = event.type == KEYUP
+            posted = None
+            key = event.key
+            for cmd, keys in self.key_map.iteritems():
+                if key in keys:
+                    self.post_command(cmd, cancel=cancel)
+                    posted = cmd
+            if (not posted or posted not in self.any_press_ignored) and \
+                   key not in self.any_press_ignored_keys:
+                self.post_any_command(key, cancel)
+
+    def post_command(self, cmd, **attributes):
+        self.delegate.post(cmd, **attributes)
+
+    def post_any_command(self, id, cancel=False):
+        self.post_command("any", id=id, cancel=cancel)
+
+    def translate_joy_button(self, event):
+        if not self.suppressed:
+            cancel = event.type == JOYBUTTONUP
+            posted = None
+            for command, button in self.joy_button_map.iteritems():
+                if button == event.button:
+                    self.post_command(command, cancel=cancel)
+                    posted = command
+            if not posted or posted not in self.any_press_ignored:
+                self.post_any_command(event.button, cancel)
+
+    def translate_axis_motion(self, event):
+        if not self.suppressed:
+            axis = event.axis
+            value = event.value
+            if not value:
+                for command in "up", "right", "down", "left":
+                    self.post_command(command, cancel=True)
+                    if command not in self.any_press_ignored:
+                        self.post_any_command(command, True)
+            else:
+                if axis == 1:
+                    if value < 0:
+                        command = "up"
+                    elif value > 0:
+                        command = "down"
+                else:
+                    if value > 0:
+                        command = "right"
+                    elif value < 0:
+                        command = "left"
+                self.post_command(command)
+                if command not in self.any_press_ignored:
+                    self.post_any_command(command)
+
+    def is_command_active(self, command):
+        if not self.suppressed:
+            if self.is_key_pressed(command):
+                return True
+            joystick = self.joystick
+            joy_map = self.joy_button_map
+            if command in joy_map and joystick.get_button(joy_map[command]):
+                return True
+            if command == "up":
+                return joystick.is_direction_pressed(Joystick.up)
+            elif command == "right":
+                return joystick.is_direction_pressed(Joystick.right)
+            elif command == "down":
+                return joystick.is_direction_pressed(Joystick.down)
+            elif command == "left":
+                return joystick.is_direction_pressed(Joystick.left)
+
+    def is_key_pressed(self, command):
+        poll = get_pressed()
+        for key in self.key_map[command]:
+            if poll[key]:
+                return True
+
+    def translate_mouse_input(self, event):
+        button = event.button
+        pos = event.pos
+        post = self.post_command
+        if event.type == MOUSEBUTTONDOWN:
+            if button == 1:
+                last = self.last_mouse_down_left
+                if last:
+                    limit = self.double_click_time_limit
+                    if get_secs() - last < limit:
+                        post("mouse-double-click-left", pos=pos)
+                last = get_secs()
+                self.last_mouse_down_left = last
+
+    def get_axes(self):
+        axes = {}
+        for direction in "up", "right", "down", "left":
+            axes[direction] = self.is_command_active(direction)
+        return axes
+
+    def register_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.update(args)
+        self.any_press_ignored_keys.update(self.extract_keys(attributes))
+
+    def extract_keys(self, attributes):
+        keys = []
+        if "keys" in attributes:
+            keys = attributes["keys"]
+            if type(keys) == int:
+                keys = [keys]
+        return keys
+
+    def unregister_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.difference_update(args)
+        self.any_press_ignored_keys.difference_update(
+            self.extract_keys(attributes))
+
+
+class Joystick:
+
+    (up, right, down, left) = range(4)
+
+    def __init__(self):
+        js = None
+        if joy.get_count() > 0:
+            js = joy.Joystick(0)
+            js.init()
+        self.js = js
+
+    def is_direction_pressed(self, direction):
+        js = self.js
+        if not js or direction > 4:
+            return False
+        if direction == 0:
+            return js.get_axis(1) < 0
+        elif direction == 1:
+            return js.get_axis(0) > 0
+        elif direction == 2:
+            return js.get_axis(1) > 0
+        elif direction == 3:
+            return js.get_axis(0) < 0
+
+    def get_button(self, id):
+       if self.js:
+           return self.js.get_button(id)
diff --git a/scale_sieve/pgfw/Interpolator.py b/scale_sieve/pgfw/Interpolator.py
new file mode 100644 (file)
index 0000000..8a578f1
--- /dev/null
@@ -0,0 +1,733 @@
+from re import match
+from os.path import join
+from tempfile import gettempdir
+
+from pygame import Surface
+from pygame.font import Font
+from pygame.draw import aaline
+from pygame.locals import *
+
+from GameChild import GameChild
+from Sprite import Sprite
+from Animation import Animation
+
+class Interpolator(list, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.set_nodesets()
+        self.gui_enabled = self.check_command_line("-interpolator")
+        if self.gui_enabled:
+            self.gui = GUI(self)
+
+    def set_nodesets(self):
+        config = self.get_configuration()
+        if config.has_section("interpolate"):
+            for name, value in config.get_section("interpolate").iteritems():
+                self.add_nodeset(name, value)
+
+    def add_nodeset(self, name, value, method=None):
+        self.append(Nodeset(name, value, method))
+        return len(self) - 1
+
+    def is_gui_active(self):
+        return self.gui_enabled and self.gui.active
+
+    def get_nodeset(self, name):
+        for nodeset in self:
+            if nodeset.name == name:
+                return nodeset
+
+    def remove(self, outgoing):
+        for ii, nodeset in enumerate(self):
+            if nodeset.name == outgoing.name:
+                self.pop(ii)
+                break
+
+
+class Nodeset(list):
+
+    LINEAR, CUBIC = range(2)
+
+    def __init__(self, name, nodes, method=None):
+        list.__init__(self, [])
+        self.name = name
+        if isinstance(nodes, str):
+            self.parse_raw(nodes)
+        else:
+            self.interpolation_method = method
+            self.parse_list(nodes)
+        self.set_splines()
+
+    def parse_raw(self, raw):
+        raw = raw.strip()
+        if raw[0].upper() == "L":
+            self.set_interpolation_method(self.LINEAR, False)
+        else:
+            self.set_interpolation_method(self.CUBIC, False)
+        for node in raw[1:].strip().split(","):
+            self.add_node(map(float, node.strip().split()), False)
+
+    def set_interpolation_method(self, method, refresh=True):
+        self.interpolation_method = method
+        if refresh:
+            self.set_splines()
+
+    def add_node(self, coordinates, refresh=True):
+        x = coordinates[0]
+        inserted = False
+        index = 0
+        for ii, node in enumerate(self):
+            if x < node.x:
+                self.insert(ii, Node(coordinates))
+                inserted = True
+                index = ii
+                break
+            elif x == node.x:
+                return None
+        if not inserted:
+            self.append(Node(coordinates))
+            index = len(self) - 1
+        if refresh:
+            self.set_splines()
+        return index
+
+    def parse_list(self, nodes):
+        for node in nodes:
+            self.add_node(node)
+
+    def set_splines(self):
+        if self.interpolation_method == self.LINEAR:
+            self.set_linear_splines()
+        else:
+            self.set_cubic_splines()
+
+    def set_linear_splines(self):
+        self.splines = splines = []
+        for ii in xrange(len(self) - 1):
+            x1, y1, x2, y2 = self[ii] + self[ii + 1]
+            m = float(y2 - y1) / (x2 - x1)
+            splines.append(LinearSpline(x1, y1, m))
+
+    def set_cubic_splines(self):
+        n = len(self) - 1
+        a = [node.y for node in self]
+        b = [None] * n
+        d = [None] * n
+        h = [self[ii + 1].x - self[ii].x for ii in xrange(n)]
+        alpha = [None] + [(3.0 / h[ii]) * (a[ii + 1] - a[ii]) - \
+                          (3.0 / h[ii - 1]) * (a[ii] - a[ii - 1]) \
+                          for ii in xrange(1, n)]
+        c = [None] * (n + 1)
+        l = [None] * (n + 1)
+        u = [None] * (n + 1)
+        z = [None] * (n + 1)
+        l[0] = 1
+        u[0] = z[0] = 0
+        for ii in xrange(1, n):
+            l[ii] = 2 * (self[ii + 1].x - self[ii - 1].x) - \
+                    h[ii - 1] * u[ii - 1]
+            u[ii] = h[ii] / l[ii]
+            z[ii] = (alpha[ii] - h[ii - 1] * z[ii - 1]) / l[ii]
+        l[n] = 1
+        z[n] = c[n] = 0
+        for jj in xrange(n - 1, -1, -1):
+            c[jj] = z[jj] - u[jj] * c[jj + 1]
+            b[jj] = (a[jj + 1] - a[jj]) / h[jj] - \
+                    (h[jj] * (c[jj + 1] + 2 * c[jj])) / 3
+            d[jj] = (c[jj + 1] - c[jj]) / (3 * h[jj])
+        self.splines = [CubicSpline(self[ii].x, a[ii], b[ii], c[ii],
+                                    d[ii]) for ii in xrange(n)]
+
+    def get_y(self, t, loop=False, reverse=False, natural=False):
+        if loop or reverse:
+            if reverse and int(t) / int(self[-1].x) % 2:
+                t = self[-1].x - t
+            t %= self[-1].x
+        elif not natural:
+            if t < self[0].x:
+                t = self[0].x
+            elif t > self[-1].x:
+                t = self[-1].x
+        splines = self.splines
+        for ii in xrange(len(splines) - 1):
+            if t < splines[ii + 1].x:
+                return splines[ii].get_y(t)
+        return splines[-1].get_y(t)
+
+    def remove(self, node, refresh=True):
+        list.remove(self, node)
+        if refresh:
+            self.set_splines()
+
+    def resize(self, left, length, refresh=True):
+        old_left = self[0].x
+        old_length = self.get_length()
+        for node in self:
+            node.x = left + length * (node.x - old_left) / old_length
+        if refresh:
+            self.set_splines()
+
+    def get_length(self):
+        return self[-1].x - self[0].x
+
+
+class Node(list):
+
+    def __init__(self, coordinates):
+        list.__init__(self, coordinates)
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+        return list.__get__(self, name)
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            list.__setitem__(self, 0, value)
+        elif name == "y":
+            list.__setitem__(self, 1, value)
+        else:
+            list.__setattr__(self, name, value)
+
+
+class Spline:
+
+    def __init__(self, x):
+        self.x = x
+
+
+class CubicSpline(Spline):
+
+    def __init__(self, x, a, b, c, d):
+        Spline.__init__(self, x)
+        self.a = a
+        self.b = b
+        self.c = c
+        self.d = d
+
+    def get_y(self, t):
+        x = self.x
+        return self.a + self.b * (t - x) + self.c * (t - x) ** 2 + self.d * \
+               (t - x) ** 3
+
+
+class LinearSpline(Spline):
+
+    def __init__(self, x, y, m):
+        Spline.__init__(self, x)
+        self.y = y
+        self.m = m
+
+    def get_y(self, t):
+        return self.m * (t - self.x) + self.y
+
+
+class GUI(Animation):
+
+    B_DUPLICATE, B_WRITE, B_DELETE, B_LINEAR, B_CUBIC, B_SPLIT = range(6)
+    S_NONE, S_LEFT, S_RIGHT = range(3)
+
+    def __init__(self, parent):
+        Animation.__init__(self, parent, unfiltered=True)
+        self.audio = self.get_audio()
+        self.display = self.get_game().display
+        self.display_surface = self.get_display_surface()
+        self.time_filter = self.get_game().time_filter
+        self.delegate = self.get_delegate()
+        self.split = self.S_NONE
+        self.success_indicator_active = True
+        self.success_indicator_blink_count = 0
+        self.load_configuration()
+        self.font = Font(None, self.label_size)
+        self.prompt = Prompt(self)
+        self.set_temporary_file()
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_marker_frame()
+        self.set_buttons()
+        self.active = False
+        self.set_nodeset_index()
+        self.set_y_range()
+        self.set_markers()
+        self.subscribe(self.respond_to_command)
+        self.subscribe(self.respond_to_mouse_down, MOUSEBUTTONDOWN)
+        self.subscribe(self.respond_to_key, KEYDOWN)
+        self.register(self.show_success_indicator, interval=100)
+        self.register(self.save_temporary_file, interval=10000)
+        self.play(self.save_temporary_file)
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.label_size = config["label-size"]
+        self.axis_label_count = config["axis-label-count"]
+        self.margin = config["margin"]
+        self.curve_color = config["curve-color"]
+        self.marker_size = config["marker-size"]
+        self.marker_color = config["marker-color"]
+        self.label_precision = config["label-precision"]
+        self.template_nodeset = config["template-nodeset"]
+        self.template_nodeset_name = config["template-nodeset-name"]
+        self.flat_y_range = config["flat-y-range"]
+
+    def set_temporary_file(self):
+        self.temporary_file = open(join(gettempdir(), "pgfw-config"), "w")
+
+    def set_background(self):
+        surface = Surface(self.display_surface.get_size())
+        surface.fill((0, 0, 0))
+        self.background = surface
+
+    def set_success_indicator(self):
+        surface = Surface((10, 10))
+        surface.fill((0, 255, 0))
+        rect = surface.get_rect()
+        rect.topleft = self.display_surface.get_rect().topleft
+        self.success_indicator, self.success_indicator_rect = surface, rect
+
+    def set_plot_rect(self):
+        margin = self.margin
+        self.plot_rect = self.display_surface.get_rect().inflate(-margin,
+                                                                 -margin)
+
+    def set_marker_frame(self):
+        size = self.marker_size
+        surface = Surface((size, size))
+        transparent_color = (255, 0, 255)
+        surface.fill(transparent_color)
+        surface.set_colorkey(transparent_color)
+        line_color = self.marker_color
+        aaline(surface, line_color, (0, 0), (size - 1, size - 1))
+        aaline(surface, line_color, (0, size - 1), (size - 1, 0))
+        self.marker_frame = surface
+
+    def set_buttons(self):
+        self.buttons = buttons = []
+        text = "Duplicate", "Write", "Delete", "Linear", "Cubic", "Split: No"
+        x = 0
+        for instruction in text:
+            buttons.append(Button(self, instruction, x))
+            x += buttons[-1].location.w + 10
+
+    def set_nodeset_index(self, increment=None, index=None):
+        parent = self.parent
+        if index is None:
+            if not increment:
+                index = 0
+            else:
+                index = self.nodeset_index + increment
+                limit = len(parent) - 1
+                if index > limit:
+                    index = 0
+                elif index < 0:
+                    index = limit
+        self.nodeset_index = index
+        self.set_nodeset_label()
+
+    def set_nodeset_label(self):
+        surface = self.font.render(self.get_nodeset().name, True, (0, 0, 0),
+                                   (255, 255, 255))
+        rect = surface.get_rect()
+        rect.bottomright = self.display_surface.get_rect().bottomright
+        self.nodeset_label, self.nodeset_label_rect = surface, rect
+
+    def get_nodeset(self):
+        if not len(self.parent):
+            self.parent.add_nodeset(self.template_nodeset_name,
+                                    self.template_nodeset)
+            self.set_nodeset_index(0)
+        return self.parent[self.nodeset_index]
+
+    def set_y_range(self):
+        width = self.plot_rect.w
+        nodeset = self.get_nodeset()
+        self.y_range = y_range = [nodeset[0].y, nodeset[-1].y]
+        x = 0
+        while x < width:
+            y = nodeset.get_y(self.get_function_coordinates(x)[0])
+            if y < y_range[0]:
+                y_range[0] = y
+            elif y > y_range[1]:
+                y_range[1] = y
+            x += width * .01
+        if y_range[1] - y_range[0] == 0:
+            y_range[1] += self.flat_y_range
+        if self.split:
+            self.adjust_for_split(y_range, nodeset)
+        self.set_axis_labels()
+
+    def get_function_coordinates(self, xp=0, yp=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        rect = self.plot_rect
+        x = float(xp) / (rect.right - rect.left) * (x_max - x_min) + x_min
+        y = float(yp) / (rect.bottom - rect.top) * (y_min - y_max) + y_max
+        return x, y
+
+    def adjust_for_split(self, y_range, nodeset):
+        middle = nodeset[0].y if self.split == self.S_LEFT else nodeset[-1].y
+        below, above = middle - y_range[0], y_range[1] - middle
+        if below > above:
+            y_range[1] += below - above
+        else:
+            y_range[0] -= above - below
+
+    def set_axis_labels(self):
+        self.axis_labels = labels = []
+        nodeset, formatted, render, rect, yr = (self.get_nodeset(),
+                                                self.get_formatted_measure,
+                                                self.font.render,
+                                                self.plot_rect, self.y_range)
+        for ii, node in enumerate(nodeset[0::len(nodeset) - 1]):
+            xs = render(formatted(node.x), True, (0, 0, 0), (255, 255, 255))
+            xsr = xs.get_rect()
+            xsr.top = rect.bottom
+            if not ii:
+                xsr.left = rect.left
+            else:
+                xsr.right = rect.right
+            ys = render(formatted(yr[ii]), True, (0, 0, 0), (255, 255, 255))
+            ysr = ys.get_rect()
+            ysr.right = rect.left
+            if not ii:
+                ysr.bottom = rect.bottom
+            else:
+                ysr.top = rect.top
+            labels.append(((xs, xsr), (ys, ysr)))
+
+    def get_formatted_measure(self, measure):
+        return "%s" % float(("%." + str(self.label_precision) + "g") % measure)
+
+    def deactivate(self):
+        self.active = False
+        self.time_filter.open()
+        self.audio.muted = self.saved_mute_state
+        self.display.set_mouse_visibility(self.saved_mouse_state)
+
+    def respond_to_command(self, event):
+        compare = self.delegate.compare
+        if compare(event, "toggle-interpolator"):
+            self.toggle()
+        elif self.active:
+            if compare(event, "reset-game"):
+                self.deactivate()
+            elif compare(event, "quit"):
+                self.get_game().end(event)
+
+    def toggle(self):
+        if self.active:
+            self.deactivate()
+        else:
+            self.activate()
+
+    def activate(self):
+        self.active = True
+        self.time_filter.close()
+        self.saved_mute_state = self.audio.muted
+        self.audio.mute()
+        self.draw()
+        self.saved_mouse_state = self.display.set_mouse_visibility(True)
+
+    def respond_to_mouse_down(self, event):
+        redraw = False
+        if self.active and not self.prompt.active:
+            nodeset_rect = self.nodeset_label_rect
+            plot_rect = self.plot_rect
+            if event.button == 1:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(1)
+                    redraw = True
+                elif self.axis_labels[0][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[0]))
+                    self.prompt.activate(text, self.resize_nodeset, 0)
+                elif self.axis_labels[1][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[-1]))
+                    self.prompt.activate(text, self.resize_nodeset, -1)
+                else:
+                    bi = self.collide_buttons(pos)
+                    if bi is not None:
+                        if bi == self.B_WRITE:
+                            self.get_configuration().write()
+                            self.play(self.show_success_indicator)
+                        elif bi in (self.B_LINEAR, self.B_CUBIC):
+                            nodeset = self.get_nodeset()
+                            if bi == self.B_LINEAR:
+                                nodeset.set_interpolation_method(Nodeset.LINEAR)
+                            else:
+                                nodeset.set_interpolation_method(Nodeset.CUBIC)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_DUPLICATE:
+                            self.prompt.activate("", self.add_nodeset)
+                        elif bi == self.B_DELETE and len(self.parent) > 1:
+                            self.parent.remove(self.get_nodeset())
+                            self.set_nodeset_index(1)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_SPLIT:
+                            self.toggle_split()
+                            redraw = True
+                    elif plot_rect.collidepoint(pos) and \
+                             not self.collide_markers(pos):
+                        xp, yp = pos[0] - plot_rect.left, pos[1] - plot_rect.top
+                        self.get_nodeset().add_node(
+                            self.get_function_coordinates(xp, yp))
+                        self.store_in_configuration()
+                        redraw = True
+            elif event.button == 3:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(-1)
+                    redraw = True
+                elif plot_rect.collidepoint(pos):
+                    marker = self.collide_markers(pos)
+                    if marker:
+                        self.get_nodeset().remove(marker.node)
+                        self.store_in_configuration()
+                        redraw = True
+        elif self.active and self.prompt.active and \
+                 not self.prompt.rect.collidepoint(event.pos):
+            self.prompt.deactivate()
+            redraw = True
+        if redraw:
+            self.set_y_range()
+            self.set_markers()
+            self.draw()
+
+    def resize_nodeset(self, text, index):
+        result = match("^\s*(-{,1}\d*\.{,1}\d*)\s+(-{,1}\d*\.{,1}\d*)\s*$",
+                       text)
+        if result:
+            try:
+                nodeset = self.get_nodeset()
+                x, y = map(float, result.group(1, 2))
+                if (index == -1 and x > nodeset[0].x) or \
+                       (index == 0 and x < nodeset[-1].x):
+                    nodeset[index].y = y
+                    if index == -1:
+                        nodeset.resize(nodeset[0].x, x - nodeset[0].x)
+                    else:
+                        nodeset.resize(x, nodeset[-1].x - x)
+                    self.store_in_configuration()
+                    self.set_y_range()
+                    self.set_axis_labels()
+                    self.set_markers()
+                    self.draw()
+                    return True
+            except ValueError:
+                return False
+
+    def collide_buttons(self, pos):
+        for ii, button in enumerate(self.buttons):
+            if button.location.collidepoint(pos):
+                return ii
+
+    def store_in_configuration(self):
+        config = self.get_configuration()
+        section = "interpolate"
+        config.clear_section(section)
+        for nodeset in self.parent:
+            code = "L" if nodeset.interpolation_method == Nodeset.LINEAR else \
+                   "C"
+            for ii, node in enumerate(nodeset):
+                if ii > 0:
+                    code += ","
+                code += " {0} {1}".format(*map(self.get_formatted_measure,
+                                               node))
+            if not config.has_section(section):
+                config.add_section(section)
+            config.set(section, nodeset.name, code)
+
+    def toggle_split(self):
+        self.split += 1
+        if self.split > self.S_RIGHT:
+            self.split = self.S_NONE
+        self.buttons[self.B_SPLIT].set_frame(["Split: No", "Split: L",
+                                              "Split: R"][self.split])
+
+    def add_nodeset(self, name):
+        nodeset = self.get_nodeset()
+        self.set_nodeset_index(index=self.parent.add_nodeset(\
+            name, nodeset, nodeset.interpolation_method))
+        self.store_in_configuration()
+        self.draw()
+        return True
+
+    def collide_markers(self, pos):
+        for marker in self.markers:
+            if marker.location.collidepoint(pos):
+                return marker
+
+    def set_markers(self):
+        self.markers = markers = []
+        for node in self.get_nodeset()[1:-1]:
+            markers.append(Marker(self, node))
+            markers[-1].location.center = self.get_plot_coordinates(*node)
+
+    def get_plot_coordinates(self, x=0, y=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        x_ratio = float(x - x_min) / (x_max - x_min)
+        rect = self.plot_rect
+        xp = x_ratio * (rect.right - rect.left) + rect.left
+        y_ratio = float(y - y_min) / (y_max - y_min)
+        yp = rect.bottom - y_ratio * (rect.bottom - rect.top)
+        return xp, yp
+
+    def draw(self):
+        display_surface = self.display_surface
+        display_surface.blit(self.background, (0, 0))
+        display_surface.blit(self.nodeset_label, self.nodeset_label_rect)
+        self.draw_axes()
+        self.draw_function()
+        self.draw_markers()
+        self.draw_buttons()
+
+    def draw_axes(self):
+        display_surface = self.display_surface
+        for xl, yl in self.axis_labels:
+            display_surface.blit(*xl)
+            display_surface.blit(*yl)
+
+    def draw_function(self):
+        rect = self.plot_rect
+        surface = self.display_surface
+        nodeset = self.get_nodeset()
+        step = 1
+        for x in xrange(rect.left, rect.right + step, step):
+            ii = x - rect.left
+            fx = nodeset.get_y(self.get_function_coordinates(ii)[0])
+            y = self.get_plot_coordinates(y=fx)[1]
+            if ii > 0:
+                aaline(surface, self.curve_color, (x - step, last_y), (x, y))
+            last_y = y
+
+    def draw_markers(self):
+        for marker in self.markers:
+            marker.update()
+
+    def draw_buttons(self):
+        for button in self.buttons:
+            button.update()
+
+    def respond_to_key(self, event):
+        if self.prompt.active:
+            prompt = self.prompt
+            if event.key == K_RETURN:
+                if prompt.callback[0](prompt.text, *prompt.callback[1]):
+                    prompt.deactivate()
+            elif event.key == K_BACKSPACE:
+                prompt.text = prompt.text[:-1]
+                prompt.update()
+                prompt.draw_text()
+            elif (event.unicode.isalnum() or event.unicode.isspace() or \
+                  event.unicode in (".", "-", "_")) and len(prompt.text) < \
+                  prompt.character_limit:
+                prompt.text += event.unicode
+                prompt.update()
+                prompt.draw_text()
+
+    def show_success_indicator(self):
+        self.draw()
+        if self.success_indicator_blink_count > 1:
+            self.success_indicator_blink_count = 0
+            self.halt(self.show_success_indicator)
+        else:
+            if self.success_indicator_active:
+                self.display_surface.blit(self.success_indicator,
+                                          self.success_indicator_rect)
+            if self.success_indicator_active:
+                self.success_indicator_blink_count += 1
+            self.success_indicator_active = not self.success_indicator_active
+
+    def save_temporary_file(self):
+        fp = self.temporary_file
+        fp.seek(0)
+        fp.truncate()
+        self.get_configuration().write(fp)
+
+    def rearrange(self):
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_markers()
+        self.set_nodeset_label()
+        self.set_axis_labels()
+        self.set_buttons()
+        self.prompt.reset()
+
+class Marker(Sprite):
+
+    def __init__(self, parent, node):
+        Sprite.__init__(self, parent)
+        self.add_frame(parent.marker_frame)
+        self.node = node
+
+
+class Button(Sprite):
+
+    def __init__(self, parent, text, left):
+        Sprite.__init__(self, parent)
+        self.set_frame(text)
+        self.location.bottomleft = left, \
+                                   self.get_display_surface().get_rect().bottom
+
+    def set_frame(self, text):
+        self.clear_frames()
+        self.add_frame(self.parent.font.render(text, True, (0, 0, 0),
+                                               (255, 255, 255)))
+
+
+class Prompt(Sprite):
+
+    def __init__(self, parent):
+        Sprite.__init__(self, parent)
+        self.load_configuration()
+        self.font = Font(None, self.text_size)
+        self.reset()
+        self.deactivate()
+
+    def deactivate(self):
+        self.active = False
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.size = config["prompt-size"]
+        self.border_color = config["prompt-border-color"]
+        self.border_width = config["prompt-border-width"]
+        self.character_limit = config["prompt-character-limit"]
+        self.text_size = config["prompt-text-size"]
+
+    def reset(self):
+        self.set_frame()
+        self.place()
+
+    def set_frame(self):
+        self.clear_frames()
+        surface = Surface(self.size)
+        self.add_frame(surface)
+        surface.fill(self.border_color)
+        width = self.border_width * 2
+        surface.fill((0, 0, 0), surface.get_rect().inflate(-width, -width))
+
+    def place(self):
+        self.location.center = self.display_surface.get_rect().center
+
+    def activate(self, text, callback, *args):
+        self.active = True
+        self.text = str(text)
+        self.callback = callback, args
+        self.update()
+        self.draw_text()
+
+    def draw_text(self):
+        surface = self.font.render(self.text, True, (255, 255, 255), (0, 0, 0))
+        rect = surface.get_rect()
+        rect.center = self.location.center
+        self.display_surface.blit(surface, rect)
diff --git a/scale_sieve/pgfw/Mainloop.py b/scale_sieve/pgfw/Mainloop.py
new file mode 100644 (file)
index 0000000..419a966
--- /dev/null
@@ -0,0 +1,108 @@
+from pygame import display
+from pygame.font import Font
+from pygame.time import get_ticks, wait
+
+from GameChild import GameChild
+
+class Mainloop(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.overflow = 0
+        self.frame_count = 1
+        self.actual_frame_duration = 0
+        self.frames_this_second = 0
+        self.last_framerate_display = 0
+        self.load_configuration()
+        self.init_framerate_display()
+        self.last_ticks = get_ticks()
+        self.stopping = False
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.target_frame_duration = config["frame-duration"]
+        self.wait_duration = config["wait-duration"]
+        self.skip_frames = config["skip-frames"]
+        self.show_framerate = config["show-framerate"]
+        self.framerate_text_size = config["framerate-text-size"]
+        self.framerate_text_color = config["framerate-text-color"]
+        self.framerate_text_background = config["framerate-text-background"]
+        self.framerate_display_flag = config["framerate-display-flag"]
+
+    def init_framerate_display(self):
+        if self.framerate_display_active():
+            screen = self.get_screen()
+            self.last_framerate_count = 0
+            self.framerate_topright = screen.get_rect().topright
+            self.display_surface = screen
+            self.font = Font(None, self.framerate_text_size)
+            self.font.set_bold(True)
+            self.render_framerate()
+
+    def framerate_display_active(self):
+        return self.check_command_line(self.framerate_display_flag) or \
+               self.show_framerate
+
+    def render_framerate(self):
+        text = self.font.render(str(self.last_framerate_count), False,
+                                self.framerate_text_color,
+                                self.framerate_text_background)
+        rect = text.get_rect()
+        rect.topright = self.framerate_topright
+        self.framerate_text = text
+        self.framerate_text_rect = rect
+
+    def run(self):
+        while not self.stopping:
+            self.advance_frame()
+            self.update_frame_duration()
+            self.update_overflow()
+        self.stopping = False
+
+    def advance_frame(self):
+        refresh = False
+        while self.frame_count > 0:
+            refresh = True
+            self.parent.frame()
+            if self.framerate_display_active():
+                self.update_framerate()
+            self.frame_count -= 1
+            if not self.skip_frames:
+                break
+        if refresh:
+            display.update()
+
+    def update_frame_duration(self):
+        last_ticks = self.last_ticks
+        actual_frame_duration = get_ticks() - last_ticks
+        last_ticks = get_ticks()
+        while actual_frame_duration < self.target_frame_duration:
+            wait(self.wait_duration)
+            actual_frame_duration += get_ticks() - last_ticks
+            last_ticks = get_ticks()
+        self.actual_frame_duration = actual_frame_duration
+        self.last_ticks = last_ticks
+
+    def update_overflow(self):
+        self.frame_count = 1
+        target_frame_duration = self.target_frame_duration
+        overflow = self.overflow
+        overflow += self.actual_frame_duration - target_frame_duration
+        while overflow > target_frame_duration:
+            self.frame_count += 1
+            overflow -= target_frame_duration
+        overflow = self.overflow
+
+    def update_framerate(self):
+        count = self.frames_this_second + 1
+        if get_ticks() - self.last_framerate_display > 1000:
+            if count != self.last_framerate_count:
+                self.last_framerate_count = count
+                self.render_framerate()
+            self.last_framerate_display = get_ticks()
+            count = 0
+        self.display_surface.blit(self.framerate_text, self.framerate_text_rect)
+        self.frames_this_second = count
+
+    def stop(self):
+        self.stopping = True
diff --git a/scale_sieve/pgfw/Profile.py b/scale_sieve/pgfw/Profile.py
new file mode 100644 (file)
index 0000000..87a74d2
--- /dev/null
@@ -0,0 +1,26 @@
+import cProfile
+from time import strftime
+from os import mkdir
+from os.path import join, exists
+
+from GameChild import GameChild
+
+class Profile(cProfile.Profile, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        cProfile.Profile.__init__(self)
+        if self.requested():
+            self.enable()
+
+    def requested(self):
+        return self.check_command_line("p")
+
+    def end(self):
+        if self.requested():
+            root = "stat/"
+            if not exists(root):
+                mkdir(root)
+            self.disable()
+            self.create_stats()
+            self.dump_stats(join(root, strftime("%Y%m%d-%H%M_%S.stat")))
diff --git a/scale_sieve/pgfw/ScreenGrabber.py b/scale_sieve/pgfw/ScreenGrabber.py
new file mode 100644 (file)
index 0000000..2058683
--- /dev/null
@@ -0,0 +1,41 @@
+from os import makedirs
+from os.path import exists, join
+from sys import exc_info
+from time import strftime
+
+from pygame import image
+
+from GameChild import *
+from Input import *
+
+class ScreenGrabber(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.subscribe(self.save_display)
+
+    def load_configuration(self):
+        config = self.get_configuration("screen-captures")
+        self.save_path = config["path"]
+        self.file_name_format = config["file-name-format"]
+        self.file_extension = config["file-extension"]
+
+    def save_display(self, event):
+        if self.delegate.compare(event, "capture-screen"):
+            directory = self.save_path
+            try:
+                if not exists(directory):
+                    makedirs(directory)
+                name = self.build_name()
+                path = join(directory, name)
+                capture = image.save(self.get_screen(), path)
+                self.print_debug("Saved screen capture to %s" % (path))
+            except:
+                self.print_debug("Couldn't save screen capture to %s, %s" %\
+                                 (directory, exc_info()[1]))
+
+    def build_name(self):
+        return "{0}.{1}".format(strftime(self.file_name_format),
+                                self.file_extension)
diff --git a/scale_sieve/pgfw/Setup.py b/scale_sieve/pgfw/Setup.py
new file mode 100644 (file)
index 0000000..04cf753
--- /dev/null
@@ -0,0 +1,131 @@
+from os import walk, remove
+from os.path import sep, join, exists, normpath
+from re import findall, sub
+from distutils.core import setup
+from distutils.command.install import install
+from pprint import pprint
+from fileinput import FileInput
+from re import sub, match
+
+from Configuration import *
+
+class Setup:
+
+    config = Configuration()
+    manifest_path = "MANIFEST"
+
+    def __init__(self):
+        pass
+
+    def remove_old_mainfest(self):
+        path = self.manifest_path
+        if exists(path):
+            remove(path)
+
+    def build_package_list(self):
+        packages = []
+        config = self.config.get_section("setup")
+        locations = [config["package-root"]] + config["additional-packages"]
+        for location in locations:
+            if exists(location):
+                for root, dirs, files in walk(location, followlinks=True):
+                    packages.append(root.replace(sep, "."))
+        return packages
+
+    def build_data_map(self):
+        include = []
+        config = self.config.get_section("setup")
+        exclude = map(normpath, config["data-exclude"])
+        for root, dirs, files in walk("."):
+            dirs = self.remove_excluded(dirs, root, exclude)
+            files = [join(root, f) for f in self.remove_excluded(files, root,
+                                                                 exclude)]
+            if files:
+                include.append((normpath(join(config["installation-path"],
+                                              root)), files))
+        return include
+
+    def remove_excluded(self, paths, root, exclude):
+        removal = []
+        for path in paths:
+            if normpath(join(root, path)) in exclude:
+                removal.append(path)
+        for path in removal:
+            paths.remove(path)
+        return paths
+
+    def translate_title(self):
+        config = self.config.get_section("setup")
+        title = config["title"].replace(" ", config["whitespace-placeholder"])
+        return sub("[^\w-]", config["special-char-placeholder"], title)
+
+    def build_description(self):
+        description = ""
+        path = self.config.get("setup", "description-file")
+        if exists(path):
+            description = "\n%s\n%s\n%s" % (file(path).read(),
+                                            "Changelog\n=========",
+                                            self.translate_changelog())
+        return description
+
+    def translate_changelog(self):
+        translation = ""
+        path = self.config.get("setup", "changelog")
+        if exists(path):
+            lines = file(path).readlines()
+            package_name = lines[0].split()[0]
+            for line in lines:
+                line = line.strip()
+                if line.startswith(package_name):
+                    version = findall("\((.*)\)", line)[0]
+                    translation += "\n%s\n%s\n" % (version, "-" * len(version))
+                elif line and not line.startswith("--"):
+                    if line.startswith("*"):
+                        translation += line + "\n"
+                    else:
+                        translation += "  " + line + "\n"
+        return translation
+
+    def setup(self, windows=[], options={}):
+       print "running setup..."
+        self.remove_old_mainfest()
+        config = self.config.get_section("setup")
+       scripts = []
+       if config["init-script"]:
+           scripts.append(config["init-script"])
+        setup(cmdclass={"install": insert_resource_path},
+              name=self.translate_title(),
+              packages=self.build_package_list(),
+              scripts=scripts,
+              data_files=self.build_data_map(),
+              requires=config["requirements"],
+              version=config["version"],
+              description=config["summary"],
+              classifiers=config["classifiers"],
+              long_description=self.build_description(),
+              license=config["license"],
+              platforms=config["platforms"],
+              author=config["contact-name"],
+              author_email=config["contact-email"],
+              url=config["url"],
+             windows=windows,
+             options=options)
+
+
+class insert_resource_path(install):
+
+    def run(self):
+        install.run(self)
+        self.edit_game_object_file()
+
+    def edit_game_object_file(self):
+        config = Configuration().get_section("setup")
+        for path in self.get_outputs():
+            if path.endswith(config["main-object"]):
+                for line in FileInput(path, inplace=True):
+                    pattern = "^ *{0} *=.*".\
+                              format(config["resource-path-identifier"])
+                    if match(pattern, line):
+                        line = sub("=.*$", "= \"{0}\"".\
+                                   format(config["installation-path"]), line)
+                    print line.strip("\n")
diff --git a/scale_sieve/pgfw/SetupWin.py b/scale_sieve/pgfw/SetupWin.py
new file mode 100644 (file)
index 0000000..232322b
--- /dev/null
@@ -0,0 +1,69 @@
+from os import makedirs, walk, sep, remove
+from os.path import join, dirname, basename, exists
+from shutil import rmtree, copy, rmtree
+from itertools import chain
+from zipfile import ZipFile
+
+import py2exe
+
+from Setup import Setup
+
+class SetupWin(Setup):
+
+    def __init__(self):
+        Setup.__init__(self)
+        self.replace_isSystemDLL()
+
+    def replace_isSystemDLL(self):
+        origIsSystemDLL = py2exe.build_exe.isSystemDLL
+        def isSystemDLL(pathname):
+            if basename(pathname).lower() in ("libogg-0.dll", "sdl_ttf.dll"):
+                return 0
+            return origIsSystemDLL(pathname)
+        py2exe.build_exe.isSystemDLL = isSystemDLL
+
+    def setup(self):
+        config = self.config.get_section("setup")
+       windows = [{}]
+       if config["init-script"]:
+           windows[0]["script"] = config["init-script"]
+       if config["windows-icon-path"]:
+           windows[0]["icon-resources"] = [(1, config["windows-icon-path"])]
+        Setup.setup(self, windows,
+                    {"py2exe": {"packages": self.build_package_list(),
+                                "dist_dir": config["windows-dist-path"]}})
+        rmtree("build")
+        self.copy_data_files()
+        self.create_archive()
+
+    def copy_data_files(self):
+       root = self.config.get("setup", "windows-dist-path")
+        for path in chain(*zip(*self.build_data_map())[1]):
+            dest = join(root, dirname(path))
+            if not exists(dest):
+                makedirs(dest)
+            copy(path, dest)
+       self.include_readme(root)
+
+    def include_readme(self, root):
+       name = "README"
+       if exists(name):
+           readme = open(name, "r")
+           reformatted = open(join(root, name + ".txt"), "w")
+           for line in open(name, "r"):
+               reformatted.write(line.rstrip() + "\r\n")
+
+    def create_archive(self):
+        config = self.config.get_section("setup")
+        title = self.translate_title() + "-" + config["version"] + "-win"
+        archive_name = title + ".zip"
+        archive = ZipFile(archive_name, "w")
+        destination = config["windows-dist-path"]
+        for root, dirs, names in walk(destination):
+            for name in names:
+                path = join(root, name)
+                archive.write(path, path.replace(destination, title + sep))
+        archive.close()
+        copy(archive_name, "dist")
+        remove(archive_name)
+        rmtree(destination)
diff --git a/scale_sieve/pgfw/Sprite.py b/scale_sieve/pgfw/Sprite.py
new file mode 100644 (file)
index 0000000..5ceb7c5
--- /dev/null
@@ -0,0 +1,358 @@
+from os import listdir
+from os.path import isfile, join
+from sys import exc_info, stdout
+from glob import glob
+from traceback import print_exc, print_stack
+
+from pygame import Color, Rect, Surface
+from pygame.image import load
+from pygame.transform import flip
+from pygame.locals import *
+
+from Animation import Animation
+from Vector import Vector
+
+class Sprite(Animation):
+
+    def __init__(self, parent, framerate=None):
+        Animation.__init__(self, parent, self.shift_frame, framerate)
+        self.frames = []
+        self.mirrored = False
+        self.hidden = False
+        self.alpha = 255
+        self.locations = [Location(self)]
+        self.framesets = [Frameset(self, framerate=framerate)]
+        self.set_frameset(0)
+        self.motion_overflow = Vector()
+        self.display_surface = self.get_display_surface()
+
+    def __getattr__(self, name):
+        if name in ("location", "rect"):
+            return self.locations[0]
+        if hasattr(Animation, "__getattr__"):
+            return Animation.__getattr__(self, name)
+        raise AttributeError, name
+
+    def set_frameset(self, identifier):
+        if isinstance(identifier, str):
+            for ii, frameset in enumerate(self.framesets):
+                if frameset.name == identifier:
+                    identifier = ii
+                    break
+        self.frameset_index = identifier
+        self.register_interval()
+        self.update_location_size()
+        if self.get_current_frameset().length() > 1:
+            self.play()
+
+    def register_interval(self):
+        self.register(self.shift_frame,
+                      interval=self.get_current_frameset().framerate)
+
+    def get_current_frameset(self):
+        return self.framesets[self.frameset_index]
+
+    def update_location_size(self):
+        size = self.get_current_frameset().rect.size
+        for location in self.locations:
+            location.size = size
+            location.fader.init_surface()
+
+    def set_framerate(self, framerate):
+        self.get_current_frameset().set_framerate(framerate)
+        self.register_interval()
+
+    def load_from_path(self, path, transparency=False, ppa=True, key=None,
+                       extension=None, omit=False):
+        if isfile(path):
+            paths = [path]
+        else:
+            if extension:
+                paths = sorted(glob(join(path, "*." + extension)))
+            else:
+                paths = [join(path, name) for name in sorted(listdir(path))]
+        for path in paths:
+            img = load(path)
+            if transparency:
+                if ppa:
+                    frame = img.convert_alpha()
+                else:
+                    frame = self.fill_colorkey(img, key)
+            else:
+                frame = img.convert()
+            self.add_frame(frame, omit)
+
+    def fill_colorkey(self, img, key=None):
+        if not key:
+            key = (255, 0, 255)
+        img = img.convert_alpha()
+        frame = Surface(img.get_size())
+        frame.fill(key)
+        frame.set_colorkey(key)
+        frame.blit(img, (0, 0))
+        return frame
+
+    def add_frame(self, frame, omit=False):
+        self.frames.append(frame)
+        frame.set_alpha(self.alpha)
+        if not omit:
+            frameset = self.get_current_frameset()
+            frameset.add_index(self.frames.index(frame))
+            self.update_location_size()
+            if frameset.length() > 1:
+                self.play()
+
+    def shift_frame(self):
+        self.get_current_frameset().shift()
+
+    def get_current_frame(self):
+        return self.frames[self.get_current_frameset().get_current_id()]
+
+    def move(self, dx=0, dy=0):
+        for location in self.locations:
+            location.move_ip(dx, dy)
+
+    def reset_motion_overflow(self):
+        for location in self.locations:
+            location.reset_motion_overflow()
+
+    def collide(self, other):
+        if not isinstance(other, Rect):
+            other = other.rect
+        for location in self.locations:
+            if location.colliderect(other):
+                return location
+
+    def mirror(self):
+        frames = self.frames
+        for ii, frame in enumerate(frames):
+             frames[ii] = flip(frame, True, False)
+        self.mirrored = not self.mirrored
+
+    def clear_frames(self):
+        self.frames = []
+        for frameset in self.framesets:
+            frameset.order = []
+            frameset.reset()
+            frameset.measure_rect()
+
+    def add_location(self, topleft=None, offset=(0, 0), count=1, base=0):
+        if topleft is not None:
+            for ii in xrange(count):
+                self.locations.append(Location(
+                    self, Rect(topleft, self.locations[0].size)))
+        else:
+            base = self.locations[base]
+            current_offset = list(offset)
+            for ii in xrange(count):
+                self.locations.append(Location(self,
+                                               base.move(*current_offset)))
+                current_offset[0] += offset[0]
+                current_offset[1] += offset[1]
+        return self.locations[-1]
+
+    def fade(self, length=0, out=None, index=None):
+        if index is None:
+            for location in self.locations:
+                location.fader.start(length, out)
+        else:
+            self.locations[index].fader.start(length, out)
+
+    def set_alpha(self, alpha):
+        self.alpha = alpha
+        for frame in self.frames:
+            frame.set_alpha(alpha)
+        for location in self.locations:
+            location.fader.set_alpha()
+
+    def add_frameset(self, order, framerate=None, name=None):
+        frameset = Frameset(self, order, framerate, name)
+        self.framesets.append(frameset)
+        return frameset
+
+    def hide(self):
+        self.hidden = True
+
+    def unhide(self):
+        self.hidden = False
+
+    def remove_locations(self, location=None):
+        if location:
+            self.locations.remove(location)
+        else:
+            self.locations = self.locations[:1]
+
+    def reverse(self, frameset=None):
+        if frameset:
+            frameset.reverse()
+        else:
+            for frameset in self.framesets:
+                frameset.reverse()
+
+    def update(self):
+        Animation.update(self)
+        self.draw()
+
+    def draw(self):
+        for location in self.locations:
+            location.fader.draw()
+
+
+class Location(Rect):
+
+    def __init__(self, sprite, rect=(0, 0, 0, 0)):
+        self.sprite = sprite
+        Rect.__init__(self, rect)
+        self.motion_overflow = Vector()
+        self.fader = Fader(self)
+
+    def move_ip(self, dx, dy):
+        if isinstance(dx, float) or isinstance(dy, float):
+            excess = self.update_motion_overflow(dx, dy)
+            Rect.move_ip(self, int(dx) + excess[0], int(dy) + excess[1])
+        else:
+            Rect.move_ip(self, dx, dy)
+
+    def update_motion_overflow(self, dx, dy):
+        overflow = self.motion_overflow
+        overflow.move(dx - int(dx), dy - int(dy))
+        excess = map(int, overflow)
+        overflow[0] -= int(overflow[0])
+        overflow[1] -= int(overflow[1])
+        return excess
+
+    def reset_motion_overflow(self):
+        self.motion_overflow.place_at_origin()
+
+
+class Fader(Surface):
+
+    def __init__(self, location):
+        self.location = location
+        self.time_filter = location.sprite.get_game().time_filter
+        self.reset()
+
+    def reset(self):
+        self.init_surface()
+        self.fade_remaining = None
+
+    def init_surface(self):
+        Surface.__init__(self, self.location.size)
+        if self.location.sprite.frames:
+            background = Surface(self.get_size())
+            sprite = self.location.sprite
+            key = sprite.get_current_frame().get_colorkey() or (255, 0, 255)
+            self.set_colorkey(key)
+            background.fill(key)
+            self.background = background
+            self.set_alpha()
+
+    def set_alpha(self, alpha=None):
+        if alpha is None:
+            alpha = self.location.sprite.alpha
+        Surface.set_alpha(self, alpha)
+
+    def start(self, length, out=None):
+        if self.fade_remaining <= 0:
+            alpha = self.get_alpha()
+            maximum = self.location.sprite.alpha
+            if out is None:
+                out = alpha == maximum
+            if out and alpha > 0 or not out and alpha < maximum:
+                self.fade_length = self.fade_remaining = length
+                self.start_time = self.time_filter.get_ticks()
+                self.fading_out = out
+
+    def draw(self):
+        sprite = self.location.sprite
+        if self.fade_remaining >= 0:
+            self.update_alpha()
+            self.clear()
+            frame = sprite.get_current_frame()
+            frame.set_alpha(255)
+            self.blit(frame, (0, 0))
+            frame.set_alpha(sprite.alpha)
+            if not sprite.hidden:
+                self.blit_to_display(self)
+        elif self.fade_remaining is None or self.get_alpha() >= sprite.alpha:
+            if self.fade_remaining >= 0:
+                self.update_alpha()
+            if not sprite.hidden:
+                self.blit_to_display(sprite.get_current_frame())
+
+    def blit_to_display(self, frame):
+        self.location.sprite.display_surface.blit(frame, self.location)
+
+    def update_alpha(self):
+        remaining = self.fade_remaining = self.fade_length - \
+                    (self.time_filter.get_ticks() - self.start_time)
+        ratio = self.fade_length and float(remaining) / self.fade_length
+        if not self.fading_out:
+            ratio = 1 - ratio
+        maximum = self.location.sprite.alpha
+        alpha = int(ratio * maximum)
+        if alpha > maximum:
+            alpha = maximum
+        elif alpha < 0:
+            alpha = 0
+        self.set_alpha(alpha)
+
+    def clear(self):
+        self.blit(self.background, (0, 0))
+
+
+class Frameset():
+
+    def __init__(self, sprite, order=[], framerate=None, name=None):
+        self.sprite = sprite
+        self.name = name
+        self.reversed = False
+        self.order = []
+        self.rect = Rect(0, 0, 0, 0)
+        self.add_index(order)
+        self.set_framerate(framerate)
+        self.reset()
+
+    def add_index(self, order):
+        if isinstance(order, int):
+            order = [order]
+        self.order += order
+        self.measure_rect()
+
+    def set_framerate(self, framerate):
+        self.framerate = framerate
+
+    def reset(self):
+        self.current_index = 0
+
+    def get_current_id(self):
+        return self.order[self.current_index]
+
+    def measure_rect(self):
+        max_width, max_height = 0, 0
+        frames = self.sprite.frames
+        for index in self.order:
+            frame = frames[index]
+            width, height = frame.get_size()
+            max_width = max(width, max_width)
+            max_height = max(height, max_height)
+        self.rect.size = max_width, max_height
+
+    def shift(self):
+        if len(self.order) > 1:
+            self.increment_index()
+
+    def increment_index(self):
+        increment = 1 if not self.reversed else -1
+        index = self.current_index + increment
+        if index < 0:
+            index = self.length() - 1
+        elif index >= self.length():
+            index = 0
+        self.current_index = index
+
+    def length(self):
+        return len(self.order)
+
+    def reverse(self):
+        self.reversed = not self.reversed
diff --git a/scale_sieve/pgfw/TimeFilter.py b/scale_sieve/pgfw/TimeFilter.py
new file mode 100644 (file)
index 0000000..e57e3c1
--- /dev/null
@@ -0,0 +1,36 @@
+from pygame.time import get_ticks
+
+from GameChild import GameChild
+
+class TimeFilter(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.ticks = self.unfiltered_ticks = self.last_ticks = get_ticks()
+        self.open()
+
+    def close(self):
+        self.closed = True
+
+    def open(self):
+        self.closed = False
+
+    def get_ticks(self):
+        return self.ticks
+
+    def get_unfiltered_ticks(self):
+        return self.unfiltered_ticks
+
+    def get_last_ticks(self):
+        return self.last_ticks
+
+    def get_last_frame_duration(self):
+        return self.last_frame_duration
+
+    def update(self):
+        ticks = get_ticks()
+        self.last_frame_duration = duration = ticks - self.last_ticks
+        if not self.closed:
+            self.ticks += duration
+        self.unfiltered_ticks += duration
+        self.last_ticks = ticks
diff --git a/scale_sieve/pgfw/Vector.py b/scale_sieve/pgfw/Vector.py
new file mode 100644 (file)
index 0000000..819e847
--- /dev/null
@@ -0,0 +1,69 @@
+class Vector(list):
+
+    def __init__(self, x=0, y=0):
+        list.__init__(self, (x, y))
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            self[0] = value
+        elif name == "y":
+            self[1] = value
+        else:
+            list.__setattr__(self, name, value)
+
+    def __add__(self, other):
+        return Vector(self.x + other[0], self.y + other[1])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        self.x += other[0]
+        self.y += other[1]
+        return self
+
+    def __sub__(self, other):
+        return Vector(self.x - other[0], self.y - other[1])
+
+    def __rsub__(self, other):
+        return Vector(other[0] - self.x, other[1] - self.y)
+
+    def __isub__(self, other):
+        self.x -= other[0]
+        self.y -= other[1]
+        return self
+
+    def __mul__(self, other):
+        return Vector(self.x * other, self.y * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        self.x *= other
+        self.y *= other
+        return self
+
+    def apply_to_components(self, function):
+        self.x = function(self.x)
+        self.y = function(self.y)
+
+    def place(self, x=None, y=None):
+        if x is not None:
+            self.x = x
+        if y is not None:
+            self.y = y
+
+    def move(self, dx=0, dy=0):
+        if dx:
+            self.x += dx
+        if dy:
+            self.y += dy
+
+    def place_at_origin(self):
+        self.x = 0
+        self.y = 0
diff --git a/scale_sieve/pgfw/VideoRecorder.py b/scale_sieve/pgfw/VideoRecorder.py
new file mode 100644 (file)
index 0000000..2aba53c
--- /dev/null
@@ -0,0 +1,71 @@
+from os import makedirs
+from os.path import exists, join
+from tempfile import TemporaryFile
+from time import strftime
+
+from pygame.image import tostring, frombuffer, save
+from pygame.time import get_ticks
+
+from GameChild import GameChild
+
+class VideoRecorder(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.display_surface = self.get_display_surface()
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.reset()
+        self.subscribe(self.respond)
+
+    def load_configuration(self):
+        config = self.get_configuration("video-recordings")
+        self.root = config["path"]
+        self.directory_name_format = config["directory-name-format"]
+        self.file_extension = config["file-extension"]
+        self.frame_format = config["frame-format"]
+        self.framerate = config["framerate"]
+
+    def reset(self):
+        self.recording = False
+        self.frame_length = None
+        self.frames = None
+        self.last_frame = 0
+
+    def respond(self, event):
+        compare = self.delegate.compare
+        if compare(event, "record-video"):
+            self.toggle_record()
+        elif compare(event, "reset-game"):
+            self.reset()
+
+    def toggle_record(self):
+        recording = not self.recording
+        if recording:
+            self.frame_length = len(self.get_string())
+            self.frames = TemporaryFile()
+        else:
+            self.write_frames()
+        self.recording = recording
+
+    def get_string(self):
+        return tostring(self.display_surface, self.frame_format)
+
+    def write_frames(self):
+        root = join(self.root, strftime(self.directory_name_format))
+        if not exists(root):
+            makedirs(root)
+        size = self.display_surface.get_size()
+        frames = self.frames
+        frames.seek(0)
+        for ii, frame in enumerate(iter(lambda: frames.read(self.frame_length),
+                                        "")):
+            path = join(root, "%04i.png" % ii)
+            save(frombuffer(frame, size, self.frame_format), path)
+        print "wrote video frames to " + root
+
+    def update(self):
+        ticks = get_ticks()
+        if self.recording and ticks - self.last_frame >= self.framerate:
+            self.frames.write(self.get_string())
+            self.last_frame = ticks
diff --git a/scale_sieve/pgfw/__init__.py b/scale_sieve/pgfw/__init__.py
new file mode 100644 (file)
index 0000000..e69de29