from panopticon_swarm.pgfw.Setup import Setup

if __name__ == "__main__":
    Setup().setup()
from panopticon_swarm.pgfw.SetupWin import SetupWin

if __name__ == "__main__":
    SetupWin.setup()
from pprint import pprint

from panopticon_swarm.pgfw.Game import Game
from panopticon_swarm.pgfw.Configuration import TypeDeclarations
from panopticon_swarm.pytmx import pytmx

class PanopticonSwarm(Game):

    def __init__(self):
        Game.__init__(self, type_declarations=Types())
        print self.get_configuration().resources_path


class Types(TypeDeclarations):

    additional_defaults = {"joy": {"int": ["action", "rotate-right",
                                           "rotate-left", "advance", "retreat",
                                           "confirm", "pause"]}}
from sys import platform


if __name__ == "__main__":
    if platform == "darwin":
        from setuptools import setup, find_packages
        from lib.pgfw.pgfw.Configuration import Configuration
        from lib.pgfw.pgfw.Setup import Setup
        config = Configuration()
        setup_obj = Setup()
        version = config.get_section("setup")["version"]
        name = setup_obj.translate_title()
        plist = dict(
            CFBundleIconFile=name,
            CFBundleName=name,
            CFBundleShortVersionString=version,
            CFBundleGetInfoString=' '.join([name, version]),
            CFBundleExecutable=name,
            CFBundleIdentifier='org.' + name.lower())
        setup(name=name,
              version=version,
              app=[dict(script="OPEN-GAME", plist=plist)],
              setup_requires=["py2app"],
              options=dict(py2app=dict(arch="i386",)),
              data_files=["wisp", "resource", "lib", "config", "OFL.txt"])
    elif platform == "win32":
        from lib.pgfw.pgfw.SetupWin import SetupWin
        SetupWin().setup()
    else:
        from lib.pgfw.pgfw.Setup import Setup
        Setup().setup()
from os.path import basename, join
from random import random, randrange, choice, randint
from glob import glob
from array import array

from pygame import PixelArray, Surface, Color, joystick
from pygame.event import clear, Event
from pygame.image import load, save
from pygame.transform import flip, scale
from pygame.mixer import Sound, set_num_channels, get_num_channels
from pygame.locals import *

from lib.pgfw.pgfw.Game import Game
from lib.pgfw.pgfw.GameChild import GameChild
from lib.pgfw.pgfw.Animation import Animation
from lib.pgfw.pgfw.Sprite import Sprite
from lib.pgfw.pgfw.extension import (get_color_swapped_surface, get_delta,
                                     get_busy_channel_count)

class PictureProcessing(Game):

    SCALE = 3
    CHANNEL_COUNT = 16
    KEYBOARD, GAMEPAD = xrange(2)

    def __init__(self):
        Game.__init__(self)
        set_num_channels(self.CHANNEL_COUNT)
        self.sound_effects = SoundEffects(self)
        self.glyphs = Glyphs(self)
        self.levels = Levels(self)
        self.interface = Interface(self)
        self.title = Title(self)
        self.introduction = Introduction(self)
        self.ending = Ending(self)
        self.subscribe(self.respond)
        self.reset()
        clear()

    def respond(self, event):
        if self.delegate.compare(event, "reset-game"):
            self.reset()

    def reset(self):
        self.title.reset()
        self.levels.reset()
        self.interface.reset()
        self.introduction.reset()
        self.ending.reset()

    def get_progress(self):
        return map(int, open(self.get_resource("progress")).read().split())

    def write_progress(self, level_index, swap_count, update=False):
        if update:
            swap_count += self.get_progress()[1]
        open(self.get_resource("progress"), "w").write("%s %s\n" % (str(level_index),
                                                                    str(swap_count)))

    def select_text(self, texts):
        return texts[self.is_gamepad_mode()]

    def is_gamepad_mode(self):
        return not self.check_command_line("-keyboard") and joystick.get_count()

    def update(self):
        self.introduction.update()
        self.title.update()
        self.levels.update()
        self.interface.update()
        self.ending.update()


class SoundEffects(GameChild):

    def __init__(self, parent):
        GameChild.__init__(self, parent)
        effects = self.effects = []
        for path in glob(join(self.get_resource("aud/effect"), "*.wav")):
            effects.append(SoundEffect(self, path))

    def play(self, name, volume=None):
        for effect in self.effects:
            if effect.name == name:
                effect.play(volume=volume)


class SoundEffect(GameChild, Sound):

    DEFAULT_VOLUME = .5

    def __init__(self, parent, path, volume=DEFAULT_VOLUME):
        GameChild.__init__(self, parent)
        Sound.__init__(self, path)
        self.name = basename(path).split(".")[0]
        self.display_surface = self.get_display_surface()
        self.set_volume(volume)

    def play(self, loops=0, maxtime=0, fade_ms=0, position=None, x=None, volume=None):
                 
        channel = Sound.play(self, loops, maxtime, fade_ms)
        if x is not None:
            position = float(x) / self.display_surface.get_width()
	if position is not None and channel is not None:
            channel.set_volume(*self.get_panning(position))
        return channel

    def get_panning(self, position):
        return 1 - max(0, ((position - .5) * 2)), \
               1 + min(0, ((position - .5) * 2))


class Glyphs(GameChild):

    TILE_SIZE = 8
    TRANSPARENT_COLOR = (255, 0, 255)
    LETTER_COLOR = (255, 255, 255)

    def __init__(self, parent):
        GameChild.__init__(self, parent)
        sheet = load(self.get_resource("font.png")).convert()
        sheet.set_colorkey(sheet.get_at((0, 0)))
        tiles = self.tiles = []
        for y in xrange(0, sheet.get_height(), self.TILE_SIZE):
            surface = Surface([self.TILE_SIZE] * 2)
            surface.set_colorkey(self.TRANSPARENT_COLOR)
            surface.fill(self.TRANSPARENT_COLOR)
            surface.blit(sheet, (0, -y))
            surface = scale(surface, [surface.get_width() * \
                                      PictureProcessing.SCALE] * 2)
            tiles.append(surface)

    def get_surface_from_text(self, text, color=None, spacing=1,
                              key=TRANSPARENT_COLOR, background=None):
        tw, th = self.tiles[0].get_size()
        surface = Surface((len(text) * tw + spacing * len(text), th))
        if background is None:
            surface.set_colorkey(key)
            surface.fill(key)
        else:
            surface.fill(background)
        for ii, x in enumerate(xrange(0, surface.get_width(), tw + spacing)):
            surface.blit(self.get_tile(ord(text[ii]), color), (x, 0))
        return surface

    def get_tile(self, index, color=None):
        tile = self.tiles[index]
        if color is not None:
            tile = tile.copy()
            pixels = PixelArray(tile)
            pixels.replace(self.LETTER_COLOR, color)
            del pixels
        return tile


class Levels(GameChild):

    CLIP_SETTINGS = ((.325, 5512), (.25, 11025), (.5, 11025), (.75, 11025), (.75, 11025))

    def __init__(self, parent):
        GameChild.__init__(self, parent)
        levels = self.levels = []
        for directory in sorted(glob(join(self.get_resource("tiles"),
                                          "[0-9]*"))):
            levels.append(Level(self, directory))

    def reset(self):
        self.deactivate()
        self.current_level = self.levels[0]

    def deactivate(self):
        self.active = False
        self.stop_audio()
        self.get_game().interface.deactivate()

    def activate(self):
        self.active = True

    def stop_audio(self):
        for level in self.levels:
            level.stop_all_audio()

    def get_current_level_index(self):
        return self.levels.index(self.current_level)

    def is_final_level(self):
        return self.get_current_level_index() == len(self.levels) - 1

    def load_next_level(self):
        self.levels[self.get_current_level_index() + 1].load()

    def update(self):
        if self.active:
            self.current_level.update()


class Level(GameChild):

    def __init__(self, parent, directory):
        GameChild.__init__(self, parent)
        self.display_surface = self.get_display_surface()
        tiles = self.tiles = []
        original = self.original_tiles = []
        for path in sorted(glob(join(directory, "*.png"))):
            original.append(load(path).convert_alpha())
            tiles.append(scale(original[-1], [original[-1].get_width() * \
                                              PictureProcessing.SCALE] * 2))
        colored = self.colored_tiles = {}
        rects = self.tile_rects = {}
        block = 0
        for line in open(join(directory, "map")):
            if line.strip() == "":
                block += 1
            elif block == 0:
                fields = line.split("-")
                if len(fields) == 1:
                    self.default_tile_index = int(fields[0])
                else:
                    for field in line.split("-"):
                        digits = map(int, field.split())
                        if len(digits) == 1:
                            index = digits[0]
                            rects[index] = []
                        elif len(digits) == 3:
                            surface = Surface(tiles[index].get_size())
                            surface.fill(digits)
                            surface.blit(tiles[index], (0, 0))
                            colored[index] = surface
                        else:
                            if len(digits) == 2:
                                digits *= 2
                            rects[index].append((digits[0:2], digits[2:4]))
        path = glob(join(self.get_resource("aud/level"), basename(directory) + "*"))[0]
        self.full_audio = Sound(path)
        self.clip_audio = None

    def load(self):
        self.parent.activate()
        self.parent.current_level = self
        self.set_audio()
        self.set_swap_status()
        self.parent.stop_audio()
        self.clip_audio.play(-1)
        self.get_game().interface.setup()

    def set_audio(self):
        samples = array("h", self.full_audio.get_buffer().raw)
        volume, count = Levels.CLIP_SETTINGS[self.parent.get_current_level_index()]
        index = randrange(0, len(samples) - count)
        clip = self.clip_audio = Sound(samples[index:index + count])
        clip.set_volume(volume)

    def set_swap_status(self):
        swap_status = self.swap_status = {}
        swapped = False
        indicies = set(self.tile_rects.keys() + [self.default_tile_index])
        while not swapped:
            available_swap_positions = indicies.copy()
            for index in indicies:
                if len(available_swap_positions) == 1 and \
                   list(available_swap_positions)[0] == index:
                    break
                while True:
                    swap_status[index] = choice(list(available_swap_positions))
                    if swap_status[index] != index:
                        break
                available_swap_positions.remove(swap_status[index])
                if not available_swap_positions:
                    swapped = True
                    break

    def stop_all_audio(self):
        if self.clip_audio:
            self.clip_audio.stop()
        self.full_audio.stop()

    def is_solved(self):
        return all(index == position for index, position in self.swap_status.iteritems())

    def update(self):
        ds = self.display_surface
        for xi, x in enumerate(xrange(0, ds.get_width(),
                                      self.tiles[0].get_width())):
            for yi, y in enumerate(xrange(0, ds.get_height(),
                                          self.tiles[0].get_height())):
                current_tile_index = self.default_tile_index
                for tile_index, rects in self.tile_rects.iteritems():
                    found = False
                    for rect in rects:
                        if rect[0][0] <= xi <= rect[1][0] and rect[0][1] <= yi \
                           <= rect[1][1]:
                            current_tile_index = tile_index
                            found = True
                            break
                    if found:
                        break
                swap_index = self.swap_status[current_tile_index]
                if swap_index in self.colored_tiles.keys():
                    tile = self.colored_tiles[swap_index]
                else:
                    tile = self.tiles[swap_index]
                ds.blit(tile, (x, y))


class Interface(Animation):

    BLINK_INTERVAL = 400
    OPEN_TEXT = "ENTER: EDIT MEMORY", "A: EDIT MEMORY"
    CELL_ALPHA = 240
    OPEN_TEXT_OFFSET = 36
    MAX_TILE_COUNT = 16
    CELL_INDENT = 32
    CELL_MARGIN = 56
    CELL_VERTICAL_PADDING = 8
    BACKGROUND = 0, 0, 0
    TEXT_COLOR = 255, 255, 255
    NAME_CHARACTER = "$"
    CELL_INDICATOR = 16
    CELL_INDICATOR_OFFSET = 2
    SHADOW_COLOR = 255, 255, 255
    SHADOW_OFFSET = -1, 1
    SWAP_INDICATOR = 16
    ADVANCE_TEXT = "PRESS ENTER", "PRESS START"
    SWAP_TEXT = "ENT: SWAP", "A: SWAP"
    HIDE_TEXT = "ESC: HIDE", "B: HIDE"
    MOVE_TEXT = ": MOVE"
    PLAY_SONG_DELAY = 1000
    UNSUPPRESS_ADVANCE_DELAY = 2000
    MOVE_ICONS = 24, 25, 26, 27

    def __init__(self, parent):
        Animation.__init__(self, parent)
        ds = self.display_surface = self.get_display_surface()
        glyphs = self.get_game().glyphs
        open_plate = self.open_plate = Sprite(self)
        open_plate.add_frame(glyphs.get_surface_from_text(self.get_game().select_text(self.OPEN_TEXT),
                                                          background=(0, 0, 0)))
        open_plate.location.midbottom = ds.get_rect().centerx, ds.get_height() - \
                                        self.OPEN_TEXT_OFFSET
        self.cell_frames = cell_frames = []
        tile_size = glyphs.get_tile(0).get_width()
        for ii in xrange(self.MAX_TILE_COUNT):
            title = ("%s%02x" % (self.NAME_CHARACTER, ii)).upper()
            title_surface = glyphs.get_surface_from_text(title, self.TEXT_COLOR)
            frame = Surface((self.CELL_INDENT * 2 + title_surface.get_width() + \
                             self.CELL_MARGIN + tile_size,
                             self.CELL_VERTICAL_PADDING * 2 + tile_size))
            frame.fill(self.BACKGROUND)
            frame.blit(title_surface, (self.CELL_INDENT, self.CELL_VERTICAL_PADDING))
            cell_frames.append(Sprite(self))
            cell_frames[-1].add_frame(frame)
            cell_frames[-1].set_alpha(self.CELL_ALPHA)
        indicator = self.cell_indicator = Sprite(self)
        indicator.add_frame(glyphs.get_tile(self.CELL_INDICATOR, self.TEXT_COLOR))
        indicator = self.swap_indicator = Sprite(self)
        indicator.add_frame(glyphs.get_tile(self.SWAP_INDICATOR, self.TEXT_COLOR))
        shadow = self.tile_shadow = Surface([tile_size] * 2)
        shadow.fill(self.SHADOW_COLOR)
        advance_plate = self.advance_plate = Sprite(self)
        advance_plate.add_frame(glyphs.get_surface_from_text(self.get_game().select_text(self.ADVANCE_TEXT),
                                                             background=(0, 0, 0)))
        advance_plate.location.midbottom = open_plate.location.midbottom
        swap_plate = self.swap_plate = Sprite(self)
        text = self.SWAP_TEXT[1] 
        swap_plate.add_frame(glyphs.get_surface_from_text(self.get_game().select_text(self.SWAP_TEXT),
                                                          background=(0, 0, 0)))
        swap_plate.set_alpha(self.CELL_ALPHA)
        move_plate = self.move_plate = Sprite(self)
        text_surface = glyphs.get_surface_from_text(self.MOVE_TEXT,
                                                    background=(0, 0, 0))
        frame = Surface((tile_size * len(self.MOVE_ICONS) + text_surface.get_width(),
                         tile_size))
        x = 0
        for index in self.MOVE_ICONS:
            frame.blit(glyphs.get_tile(index), (x, 0))
            x += tile_size
        frame.blit(text_surface, (x, 0))
        move_plate.add_frame(frame)
        move_plate.set_alpha(self.CELL_ALPHA)
        move_plate.location.midtop = ds.get_width() / 2, 0
        hide_plate = self.hide_plate = Sprite(self)
        hide_plate.add_frame(glyphs.get_surface_from_text(self.get_game().select_text(self.HIDE_TEXT),
                                                          background=(0, 0, 0)))
        hide_plate.set_alpha(self.CELL_ALPHA)
        hide_plate.location.topright = ds.get_width(), 0
        self.register(self.blink, interval=self.BLINK_INTERVAL)
        self.register(self.play_song)
        self.register(self.unsuppress_advance)
        self.subscribe(self.respond)

    def blink(self):
        if self.closed and not self.solved:
            self.open_plate.toggle_hidden()
        if not self.closed and self.swapping_index is not None:
            self.swap_indicator.toggle_hidden()
        if self.solved and not self.suppressing_commands:
            self.advance_plate.toggle_hidden()

    def respond(self, event, suppress_sound=False):
        if self.active and not self.suppressing_commands:
            delegate = self.get_game().delegate
            effects = self.get_game().sound_effects
            is_pad_mode = self.get_game().is_gamepad_mode()
            if self.closed and not self.solved and delegate.compare(event, "action"):
                effects.play("memory")
                self.unclose()
            elif self.closed and self.solved and (delegate.compare(event, "advance") or \
               (not is_pad_mode and delegate.compare(event, "action"))):
                effects.play("go")
                if self.get_game().levels.is_final_level():
                    self.get_game().ending.activate()
                    self.get_game().levels.deactivate()
                else:
                    self.get_game().levels.load_next_level()
            elif not self.closed:
                if delegate.compare(event, "cancel"):
                    effects.play("cancel")
                    self.close()
                elif delegate.compare(event, "action"):
                    if self.swapping_index is None:
                        effects.play("start")
                        self.swapping_index = self.cell_index
                        self.set_indicator_position(False)
                        self.respond(Event(delegate.command_event_id,
                                           {"command": "down", "cancel": False}), True)
                    else:
                        if self.cell_index == self.swapping_index:
                            effects.play("cancel")
                        else:
                            level = self.get_game().levels.current_level
                            status = level.swap_status
                            temp = status[self.swapping_index]
                            status[self.swapping_index] = status[self.cell_index]
                            status[self.cell_index] = temp
                            self.swap_count += 1
                            if not level.is_solved():
                                effects.play("swap")
                            else:
                                effects.play("clear")
                                if not self.get_game().levels.is_final_level():
                                    self.get_game().write_progress(self.get_game().levels.\
                                                                   get_current_level_index() + 1,
                                                                   self.swap_count, True)
                                else:
                                    self.get_game().ending.\
                                        set_swap_count(self.get_game().get_progress()[1] + \
                                                       self.swap_count)
                                    self.get_game().write_progress(0, 0)
                                self.suppressing_commands = True
                                self.solved = True
                                self.play(self.play_song, delay=self.PLAY_SONG_DELAY, play_once=True)
                        self.swapping_index = None
                elif delegate.compare(event, ["up", "down", "left", "right"]):
                    if not suppress_sound:
                        effects.play("menu-move")
                    cell_count = self.get_cell_count()
                    bp = (cell_count - 1) / 2
                    if delegate.compare(event, "up"):
                        self.cell_index -= 1
                        if self.cell_index == -1:
                            self.cell_index = bp
                        elif self.cell_index == bp:
                            self.cell_index = cell_count - 1
                    elif delegate.compare(event, "down"):
                        self.cell_index += 1
                        if self.cell_index == bp + 1:
                            self.cell_index = 0
                        elif self.cell_index == cell_count:
                            self.cell_index = bp + 1
                    elif delegate.compare(event, "left"):
                        if self.cell_index == 0:
                            self.cell_index = bp + 1
                        elif self.cell_index > bp:
                            self.cell_index -= bp + 1
                        else:
                            self.cell_index += bp
                            if self.cell_index == cell_count:
                                self.cell_index -= 1
                    elif delegate.compare(event, "right"):
                        if self.cell_index == cell_count - 1:
                            self.cell_index = bp
                        elif self.cell_index <= bp:
                            self.cell_index += bp + 1
                            if self.cell_index == cell_count:
                                self.cell_index -= 1
                        else:
                            self.cell_index -= bp
                    self.set_indicator_position()

    def play_song(self):
        self.close()
        level = self.get_game().levels.current_level
        level.clip_audio.stop()
        level.full_audio.play(-1)
        self.play(self.unsuppress_advance, delay=self.UNSUPPRESS_ADVANCE_DELAY,
                  play_once=True)

    def unsuppress_advance(self):
        self.suppressing_commands = False
        self.advance_plate.unhide()

    def set_indicator_position(self, cell=True):
        if cell:
            indicator = self.cell_indicator
        else:
            indicator = self.swap_indicator
        indicator.location.midleft = self.cell_frames[self.cell_index].location.\
                                     midleft
        indicator.location.left += self.CELL_INDICATOR_OFFSET

    def get_cell_count(self):
        return len(self.get_game().levels.current_level.swap_status)

    def reset(self):
        self.deactivate()
        self.solved = False
        self.close()
        self.halt()
        self.suppressing_commands = False

    def deactivate(self):
        self.active = False

    def setup(self):
        self.activate()
        self.close()
        cell_count = self.get_cell_count()
        left_count = cell_count / 2 + cell_count % 2
        right_count = cell_count / 2
        sw, sh = self.display_surface.get_size()
        step = sh / (left_count + 1)
        frames = self.cell_frames
        index = 0
        for y in xrange(step, step * (left_count + 1), step):
            frames[index].location.midleft = 0, y
            index += 1
        step = sh / (right_count + 1)
        for y in xrange(step, step * (right_count + 1), step):
            frames[index].location.midright = sw, y
            index += 1
        self.cell_index = 0
        self.set_indicator_position()
        self.swapping_index = None
        self.swap_count = 0
        self.suppressing_commands = False
        self.solved = False
        self.advance_plate.hide()
        self.play(self.blink)

    def activate(self):
        self.active = True

    def close(self):
        self.closed = True
        if not self.solved:
            self.open_plate.unhide()

    def unclose(self):
        self.closed = False
        self.open_plate.hide()

    def update(self):
        Animation.update(self)
        if self.active:
            self.open_plate.update()
            self.advance_plate.update()
            if not self.closed:
                for frame in self.cell_frames[:self.get_cell_count()]:
                    frame.update()
                self.cell_indicator.update()
                if self.swapping_index is not None:
                    self.swap_indicator.update()
                level = self.get_game().levels.current_level
                for index, position in level.swap_status.iteritems():
                    if position in level.colored_tiles.keys():
                        tile = level.colored_tiles[position]
                    else:
                        tile = level.tiles[position]
                    rect = tile.get_rect()
                    frame = self.cell_frames[index]
                    rect.topright = frame.location.right - self.CELL_INDENT, \
                                    frame.location.top + self.CELL_VERTICAL_PADDING
                    self.display_surface.blit(self.tile_shadow, rect.move(self.SHADOW_OFFSET))
                    self.display_surface.blit(tile, rect)
                self.swap_plate.update()
                self.hide_plate.update()
                self.move_plate.update()


class Title(Animation):

    BLINK_INTERVAL = 400
    MENU_OFFSET = 100
    MENU_WIDTH = 440
    BACKGROUND_SIZE = 40
    ADVANCE_TEXT = "PRESS ENTER", "PRESS START"
    MENU_OPTIONS = "NEW GAME", "CONTINUE"
    MENU_MARGIN = 12
    MENU_PADDING = 16
    TRANSPARENT_COLOR = (128, 128, 128)
    TURN_PROBABILITY = .0001
    SCROLL_SPEED = 2
    BACKGROUND_BACKGROUND = (0, 0, 0)
    BACKGROUND_FRAME_COUNT = 1
    BACKGROUND_FRAMERATE = 200
    MENU_INDICATOR_LOCATION = 224
    CHUNK_PROBABILITY = .5
    CHUNK_VOLUME = .1

    def __init__(self, parent):
        Animation.__init__(self, parent)
        self.display_surface = self.get_display_surface()
        self.set_audio()
        self.set_advance_plate()
        self.register(self.blink, interval=self.BLINK_INTERVAL)
        self.subscribe(self.respond)
        self.play(self.blink)

    def set_audio(self):
        self.audio_loop = Sound(self.get_resource("aud/title.wav"))
        chunks = self.audio_chunks = []
        for path in glob(join(self.get_resource("aud/chunk"), "*.ogg")):
            chunks.append(Sound(path))
            chunks[-1].set_volume(self.CHUNK_VOLUME)
        self.advance_effect = SoundEffect(self, "start")
        self.move_effect = SoundEffect(self, "menu-move")
        self.cancel_effect = SoundEffect(self, "cancel")

    def set_advance_plate(self):
        offset = 0
        plate = self.advance_plate = Sprite(self, self.BLINK_INTERVAL)
        plate.add_frame(self.get_game().glyphs.\
                        get_surface_from_text(self.get_game().select_text(self.ADVANCE_TEXT),
                                              (255, 255, 255), background=(0, 0, 0)))
        rect = self.display_surface.get_rect()
        plate.location.center = rect.centerx, rect.bottom - self.MENU_OFFSET

    def blink(self):
        self.advance_plate.toggle_hidden()
        
    def reset(self):
        self.set_background()
        self.activate()
        self.advance_pressed = False
        self.advance_plate.unhide()
        self.set_scroll_step()
        self.menu_index = 0

    def set_background(self):
        tiles = []
        for level in self.get_game().levels.levels:
            tiles.extend(level.tiles)
        background = self.background = Sprite(self, self.BACKGROUND_FRAMERATE)
        tw = tiles[0].get_width()
        frames = []
        for _ in xrange(self.BACKGROUND_FRAME_COUNT):
            frames.append(Surface([self.BACKGROUND_SIZE * tw] * 2))
            frames[-1].fill(self.BACKGROUND_BACKGROUND)
        for x in xrange(0, frames[0].get_width(), tw):
            for y in xrange(0, frames[0].get_width(), tw):
                for frame in frames:
                    tile = choice(tiles)
                    if random() >= .5:
                        inverted = Surface((tw, tw))
                        inverted.fill((255, 255, 255))
                        inverted.blit(tile, (0, 0), None, BLEND_SUB)
                        tile = inverted
                    fx, fy = random() >= .5, random() >= .5
                    if fx or fy:
                        tile = flip(tile, fx, fy)
                    frame.blit(tile, (x, y))
        for frame in frames:
            background.add_frame(frame)
        background.add_location(offset=(background.location.w, 0))
        background.add_location(offset=(0, background.location.h))
        background.add_location(offset=(background.location.w,
                                        background.location.h))

    def activate(self):
        self.active = True
        # self.audio_loop.play(-1)
        self.set_menu()

    def set_menu(self):
        glyphs = self.get_game().glyphs
        if self.get_game().get_progress()[0] > 0:
            option_texts = self.MENU_OPTIONS
        else:
            option_texts = self.MENU_OPTIONS[:1]
        height = glyphs.get_tile(0).get_height() * len(option_texts) + \
                 self.MENU_MARGIN * (len(option_texts) - 1) + \
                 self.MENU_PADDING * 2 + 3
        background = self.menu_background = Surface((int(self.MENU_WIDTH),
                                                     height))
        background.fill((0, 0, 0))
        rect = self.menu_rect = background.get_rect()
        rect.center = self.advance_plate.location.center
        options = self.options = []
        for option in option_texts:
            options.append(Sprite(self))
            options[-1].add_frame(glyphs.get_surface_from_text(option))
            options[-1].location.centerx = rect.centerx
            options[-1].location.top = rect.top + self.MENU_PADDING + \
                                       (len(options) - 1) * options[0].location.h + \
                                       (len(options) - 1) * self.MENU_MARGIN
        indicator = self.indicator = Sprite(self)
        indicator.add_frame(self.get_game().glyphs.get_tile(16))

    def set_scroll_step(self):
        self.scroll_step = get_delta(randrange(0, 360), self.SCROLL_SPEED)

    def respond(self, event):
        if self.active:
            delegate = self.get_game().delegate
            if not self.advance_pressed:
                if delegate.compare(event, "advance") or not self.get_game().is_gamepad_mode() and \
                   delegate.compare(event, "action"):
                    self.advance_pressed = True
                    self.get_game().sound_effects.play("start")
                elif delegate.compare(event, "cancel"):
                    self.get_game().sound_effects.play("cancel")
            else:
                if delegate.compare(event, "cancel"):
                    self.get_game().sound_effects.play("cancel")
                    self.advance_pressed = False
                elif delegate.compare(event, "up"):
                    self.get_game().sound_effects.play("menu-move")
                    self.menu_index -= 1
                elif delegate.compare(event, "down"):
                    self.get_game().sound_effects.play("menu-move")
                    self.menu_index += 1
                elif delegate.compare(event, "advance") or not self.get_game().is_gamepad_mode() and \
                   delegate.compare(event, "action"):
                    self.get_game().sound_effects.play("start")
                    if self.menu_index == 0:
                        self.deactivate()
                        self.get_game().introduction.load()
                        self.get_game().write_progress(0, 0)
                    elif self.menu_index == 1:
                        self.deactivate()
                        self.get_game().levels.\
                            levels[self.get_game().get_progress()[0]].load()
                    else:
                        pass
                    event.dict["command"] = ""
                if self.menu_index < 0:
                    self.menu_index = len(self.options) - 1
                elif self.menu_index >= len(self.options):
                    self.menu_index = 0

    def deactivate(self):
        self.active = False
        self.audio_loop.stop()

    def update(self):
        Animation.update(self)
        if self.active:
            if random() < self.TURN_PROBABILITY:
                self.set_scroll_step()
            self.background.move(*self.scroll_step)
            if self.background.location.top > 0:
                self.background.move(dy=-self.background.location.h)
            if self.background.location.right < 0:
                self.background.move(self.background.location.w)
            if self.background.location.bottom < 0:
                self.background.move(dy=self.background.location.h)
            if self.background.location.left > 0:
                self.background.move(-self.background.location.w)
            self.background.update()
            if not self.advance_pressed:
                self.advance_plate.update()
            else:
                self.display_surface.blit(self.menu_background, self.menu_rect)
                for option in self.options:
                    option.update()
                self.indicator.location.centery = self.options[self.menu_index]\
                                                      .location.centery
                self.indicator.location.right = self.MENU_INDICATOR_LOCATION
                self.indicator.update()
            if random() < self.CHUNK_PROBABILITY and \
               get_busy_channel_count() < get_num_channels() - 5:
                for _ in xrange(randint(1, 4)):
                    chunk = choice(self.audio_chunks)
                    channel = chunk.play()
                    if channel is not None:
                        channel.set_volume(random(), random())


class Introduction(Animation):

    BACKGROUND = (0, 0, 0)
    TEXT_COLOR = (255, 255, 255)
    INDICATOR_INDEX = 31
    TEXT = "MEDITATING.", "MEDITATING..", "MEDITATING...", \
           "MANIPULATION OF THE/SYSTEM'S PPU GRANTED"
    INDENT = 48
    OFFSET = 128
    MARGIN = 12
    BLINK_INTERVAL = 400

    def __init__(self, parent):
        Animation.__init__(self, parent)
        ds = self.display_surface = self.get_display_surface()
        background = self.background = Surface(ds.get_size())
        background.fill(self.BACKGROUND)
        blocks = self.blocks = []
        glyphs = self.get_game().glyphs
        tile_height = glyphs.get_tile(0).get_height()
        for block in self.TEXT:
            blocks.append([])
            for ii, line in enumerate(block.split("/")):
                plate = Sprite(self)
                plate.add_frame(glyphs.\
                                get_surface_from_text(line, self.TEXT_COLOR,
                                                      background=self.BACKGROUND))
                plate.location.left = self.INDENT
                plate.location.top = ds.get_height() - self.OFFSET + ii * \
                                     (tile_height + self.MARGIN)
                blocks[-1].append(plate)
        indicator = self.indicator = Sprite(self)
        indicator.add_frame(glyphs.get_tile(self.INDICATOR_INDEX, self.TEXT_COLOR))
        indicator.location.top = ds.get_height() - self.OFFSET + 2 * \
                                 (tile_height + self.MARGIN)
        indicator.location.right = ds.get_width() - self.INDENT
        self.register(self.blink, interval=self.BLINK_INTERVAL)
        self.subscribe(self.respond)
        self.play(self.blink)

    def respond(self, event):
        if self.active:
            if self.get_game().delegate.compare(event, "action"):
                self.block_index += 1
                if self.block_index == len(self.blocks):
                    self.get_game().sound_effects.play("go")
                    self.deactivate()
                    self.get_game().levels.levels[0].load()
                elif self.block_index == len(self.blocks) - 1:
                    self.get_game().sound_effects.play("granted")
                else:
                    self.get_game().sound_effects.play("next")

    def deactivate(self):
        self.active = False

    def load(self):
        self.activate()
        self.block_index = 0
        self.indicator.unhide()

    def activate(self):
        self.active = True

    def blink(self):
        self.indicator.toggle_hidden()

    def reset(self):
        self.deactivate()

    def update(self):
        Animation.update(self)
        if self.active:
            ds = self.display_surface
            ds.blit(self.background, (0, 0))
            for block in self.blocks[self.block_index]:
                block.update()
            self.indicator.update()


class Ending(Animation):

    GLYPH_RANGE = 1, 31
    SCROLL_SPEED = 2
    TURN_PROBABILITY = .0001
    PLATE_SIZE = 312, 72
    PLATE_BACKGROUND = 0, 0, 0
    TEXT_COLOR = 255, 255, 255
    END_TEXT = "THE END!"
    SWAP_COUNT_TEXT = "SWAPS:"
    UNSUPPRESS_DELAY = 1000

    def __init__(self, parent):
        Animation.__init__(self, parent)
        ds = self.display_surface = self.get_display_surface()
        end_plate = self.end_plate = Sprite(self)
        frame = Surface(self.PLATE_SIZE)
        frame.fill(self.PLATE_BACKGROUND)
        text_surface = self.get_game().glyphs.get_surface_from_text(self.END_TEXT,
                                                                    self.TEXT_COLOR)
        rect = text_surface.get_rect()
        rect.center = frame.get_rect().center
        frame.blit(text_surface, rect)
        end_plate.add_frame(frame)
        end_plate.location.center = ds.get_rect().centerx, 3 * ds.get_height() / 8
        self.audio = Sound(self.get_resource("aud/end.ogg"))
        self.subscribe(self.respond)
        self.register(self.unsuppress_advance)

    def respond(self, event):
        if self.active and not self.suppress_commands and \
           ((self.get_game().is_gamepad_mode() and self.get_game().delegate.compare(event, "advance")) or \
            (not self.get_game().is_gamepad_mode() and self.get_game().delegate.compare(event, "action"))):
            self.deactivate()
            self.get_game().reset()

    def unsuppress_advance(self):
        self.suppress_commands = False

    def set_swap_count(self, count):
        self.swap_count = count

    def reset(self):
        self.deactivate()
        self.halt()

    def deactivate(self):
        self.active = False
        self.audio.stop()

    def activate(self):
        self.active = True
        self.set_swap_plate()
        self.set_background()
        self.set_scroll_step()
        self.suppress_commands = True
        self.play(self.unsuppress_advance, delay=self.UNSUPPRESS_DELAY,
                  play_once=True)
        self.audio.play(-1)

    def set_swap_plate(self):
        swap_count_plate = self.swap_count_plate = Sprite(self)
        frame = Surface(self.PLATE_SIZE)
        frame.fill(self.PLATE_BACKGROUND)
        text_surface = self.get_game().glyphs.\
                       get_surface_from_text("%s %i" % (self.SWAP_COUNT_TEXT,
                                                        self.swap_count), self.TEXT_COLOR)
        rect = text_surface.get_rect()
        rect.center = frame.get_rect().center
        frame.blit(text_surface, rect)
        swap_count_plate.add_frame(frame)
        swap_count_plate.location.center = self.display_surface.get_rect().centerx, \
                                           5 * self.display_surface.get_height() / 8

    def set_background(self):
        background = self.background = Sprite(self)
        glyphs = self.get_game().glyphs
        tw = glyphs.get_tile(0).get_width()
        ds = self.get_display_surface()
        frame = Surface(ds.get_size())
        for xi, x in enumerate(xrange(0, ds.get_width(), tw)):
            for yi, y in enumerate(xrange(0, ds.get_height(), tw)):
                background_color = Color(0, 0, 0)
                background_color.hsla = randrange(0, 360), 100, 50, 100
                foreground_color = Color(0, 0, 0)
                foreground_color.hsla = (background_color.hsla[0] + 180) % 360, \
                                        100, 50, 100
                surface = Surface([tw] * 2)
                surface.fill(background_color)
                surface.blit(glyphs.get_tile(randint(*self.GLYPH_RANGE)), (0, 0))
                frame.blit(surface, (x, y))
        background.add_frame(frame)
        background.add_location(offset=(background.location.w, 0))
        background.add_location(offset=(0, background.location.h))
        background.add_location(offset=(background.location.w,
                                        background.location.h))
                
    def set_scroll_step(self):
        self.scroll_step = get_delta(randrange(0, 360), self.SCROLL_SPEED)

    def update(self):
        Animation.update(self)
        if self.active:
            if random() < self.TURN_PROBABILITY:
                self.set_scroll_step()
            self.background.move(*self.scroll_step)
            if self.background.location.top > 0:
                self.background.move(dy=-self.background.location.h)
            if self.background.location.right < 0:
                self.background.move(self.background.location.w)
            if self.background.location.bottom < 0:
                self.background.move(dy=self.background.location.h)
            if self.background.location.left > 0:
                self.background.move(-self.background.location.w)
            self.background.update()
            self.end_plate.update()
            self.swap_count_plate.update()
3.14.134.206
3.14.134.206
3.14.134.206
 
January 23, 2021

I wanted to document this chat-controlled robot I made for Babycastles' LOLCAM📸 that accepts a predefined set of commands like a character in an RPG party 〰 commands like walk, spin, bash, drill. It can also understand donut, worm, ring, wheels, and more. The signal for each command is transmitted as a 24-bit value over infrared using two Arduinos, one with an infrared LED, and the other with an infrared receiver. I built the transmitter circuit, and the receiver was built into the board that came with the mBot robot kit. The infrared library IRLib2 was used to transmit and receive the data as a 24-bit value.


fig. 1.1: the LEDs don't have much to do with this post!

I wanted to control the robot the way the infrared remote that came with the mBot controlled it, but the difference would be that since we would be getting input from the computer, it would be like having a remote with an unlimited amount of buttons. The way the remote works is each button press sends a 24-bit value to the robot over infrared. Inspired by Game Boy Advance registers and tracker commands, I started thinking that if we packed multiple parameters into the 24 bits, it would allow a custom move to be sent each time, so I wrote transmitter and receiver code to process commands that looked like this:

bit
name
description
00
time
multiply by 64 to get duration of command in ms
01
02
03
04
left
multiply by 16 to get left motor power
05
06
07
08
right
multiply by 16 to get right motor power
09
10
11
12
left sign
0 = left wheel backward, 1 = left wheel forward
13
right sign
0 = right wheel forward, 1 = right wheel backward
14
robot id
0 = send to player one, 1 = send to player two
15
flip
negate motor signs when repeating command
16
repeats
number of times to repeat command
17
18
19
delay
multiply by 128 to get time between repeats in ms
20
21
22
23
swap
swap the motor power values on repeat
fig 1.2: tightly stuffed bits

The first command I was able to send with this method that seemed interesting was one that made the mBot do a wheelie.

$ ./send_command.py 15 12 15 1 0 0 0 7 0 1
sending 0xff871fcf...


fig 1.3: sick wheels

A side effect of sending the signal this way is any button on any infrared remote will cause the robot to do something. The star command was actually reverse engineered from looking at the code a random remote button sent. For the robot's debut, it ended up with 15 preset commands (that number is in stonks 📈). I posted a highlights video on social media of how the chat controls turned out.

This idea was inspired by a remote frog tank LED project I made for Ribbit's Frog World which had a similar concept: press a button, and in a remote location where 🐸 and 🐠 live, an LED would turn on.


fig 2.1: saying hi to froggo remotely using an LED

😇 The transmitter and receiver Arduino programs are available to be copied and modified 😇