<commit_before>'''General action classes.'''

import sys

# TODO: Perhaps _model would be more clear if it were _app or _game.
class Action(object):

    def __init__(self, situation, name, suffix=None):
        # situation is like "outdoors" or "indoors"
        self.situation = situation

        # model is subclass of game
        self._model = None if situation is None else situation.game()

        self.name   = name
        self.suffix = suffix

    # TODO: Where is this used?
    def set_situation(self, situation):
        '''Set the containing situation for this action'''
        self.situation = situation
        self._model = situation.game()

    def model(self):
        '''getting for model (a subclass o game)'''
        return self._model

    def info(self):
        '''placeholder: generic return a string describing this action'''
        return ''

    # TODO: While swapping out GUIs/Windows, will this still be meaningful?
    def always_known(self):
        '''Is this action Always Known in this game?'''
        return False

    def can_do(self):
        '''Each action will have to define a method that determines
        if the action is currently valid or not.
        '''
        raise 'Not implemented'

    def do(self):
        '''Each Action will have to define a method that will
        actually perform some changes to the Game or UI
        '''
        raise 'Not implemented'

    def execute(self):
        '''Convience method, checks if the action can be performed,
        if so it does it.
        '''
        if not self.can_do():
            return False

        self.do()

        return True

    def __str__(self):
        if self.suffix is None:
            return self.name

        return '%s: %s' % (self.name, self.suffix)

class QuitAction(Action):

    def __init__(self, situtation):
        Action.__init__(self, situtation, 'Quit')

    def can_do(self):
        '''You can always exit the program.
        Break this functionality, and your users will hate you.
        '''
        return True

    def do(self):
        '''In this simple implementation, no window pops up
        to ask you if you're sure or if you want to save the game first.
        '''
        sys.exit()

class NullAction(Action):

    def __init__(self, situtation):
        Action.__init__(self, situtation, 'Do Nothing')

    def can_do(self):
        '''Doing nothing is always an option.'''
        return True

    def do(self):
        '''Sometimes the best thing you can do is nothing.'''
        pass
<commit_msg>#18 changed action model to game<commit_after>'''General action classes.'''

import sys

class Action(object):

    def __init__(self, situation, name, suffix=None):
        '''Actions subclass nothing, but require situations to hold them'''
        # a situation is like "outdoors", "indoors", or "fighting"
        self.situation = situation
        self._game = None if situation is None else situation.game()

        self.name   = name
        self.suffix = suffix

    # TODO: Where is this used?
    def set_situation(self, situation):
        '''Set the containing situation for this action'''
        self.situation = situation
        self._game = situation.game()

    def game(self):
        '''getting the current subclass of game'''
        return self._game

    def info(self):
        '''placeholder: generic return a string describing this action'''
        return ''

    # TODO: While swapping out GUIs/Windows, will this still be meaningful?
    def always_known(self):
        '''Is this action Always Known in this game?'''
        return False

    def can_do(self):
        '''Each action will have to define a method that determines
        if the action is currently valid or not.
        '''
        raise 'Not implemented'

    def do(self):
        '''Each Action will have to define a method that will
        actually perform some changes to the Game or UI
        '''
        raise 'Not implemented'

    def execute(self):
        '''Convience method, checks if the action can be performed,
        if so it does it.
        '''
        if not self.can_do():
            return False

        self.do()

        return True

    def __str__(self):
        if self.suffix is None:
            return self.name

        return '%s: %s' % (self.name, self.suffix)

class QuitAction(Action):

    def __init__(self, situtation):
        Action.__init__(self, situtation, 'Quit')

    def can_do(self):
        '''You can always exit the program.
        Break this functionality, and your users will hate you.
        '''
        return True

    def do(self):
        '''In this simple implementation, no window pops up
        to ask you if you're sure or if you want to save the game first.
        '''
        sys.exit()

class NullAction(Action):

    def __init__(self, situtation):
        Action.__init__(self, situtation, 'Do Nothing')

    def can_do(self):
        '''Doing nothing is always an option.'''
        return True

    def do(self):
        '''Sometimes the best thing you can do is nothing.'''
        pass
<|endoftext|>


<commit_before>#!/usr/bin/env python

# Copyright 2016 University of Chicago
# Licensed under the APL 2.0 license

import log
import log.handlers
import os

LOG_FILENAME = '~/logs/fsurf.log'
MAX_BYTES = 1024*1024*50  # 50 MB
NUM_BACKUPS = 10  # 10 files

def initialize_logging():
    """
    Initialize logging for fsurf

    :return: None
    """
    logger = log.getLogger('fsurf')
    log_file = os.path.abspath(os.path.expanduser(LOG_FILENAME))
    handle = log.handlers.RotatingFileHandler(log_file,
                                              mode='a',
                                              maxBytes=MAX_BYTES,
                                              backupCount=NUM_BACKUPS)
    handle.setLevel(log.WARN)
    logger.addHandler(handle)

def set_debugging():
    """
    Configure logging to output debug messages

    :return: None
    """
    logger = log.getLogger('fsurf')
    log_file = os.path.abspath(os.path.expanduser('~/logs/fsurf_debug.log'))
    handle = log.FileHandler(log_file)
    handle.setLevel(log.DEBUG)
    logger.addHandler(handle)

def get_logger():
    """
    Get logger that can be used for logging

    :return: logger object
    """
    return log.getLogger('fsurf')<commit_msg>Fix issues due to module rename<commit_after>#!/usr/bin/env python

# Copyright 2016 University of Chicago
# Licensed under the APL 2.0 license

import logging
import logging.handlers
import os

LOG_FILENAME = '~/logs/fsurf.log'
MAX_BYTES = 1024*1024*50  # 50 MB
NUM_BACKUPS = 10  # 10 files

def initialize_logging():
    """
    Initialize logging for fsurf

    :return: None
    """
    logger = logging.getLogger('fsurf')
    log_file = os.path.abspath(os.path.expanduser(LOG_FILENAME))
    handle = logging.handlers.RotatingFileHandler(log_file,
                                              mode='a',
                                              maxBytes=MAX_BYTES,
                                              backupCount=NUM_BACKUPS)
    handle.setLevel(logging.WARN)
    logger.addHandler(handle)

def set_debugging():
    """
    Configure logging to output debug messages

    :return: None
    """
    logger = logging.getLogger('fsurf')
    log_file = os.path.abspath(os.path.expanduser('~/logs/fsurf_debug.log'))
    handle = logging.FileHandler(log_file)
    handle.setLevel(logging.DEBUG)
    logger.addHandler(handle)

def get_logger():
    """
    Get logger that can be used for logging

    :return: logger object
    """
    return logging.getLogger('fsurf')
<|endoftext|>


<commit_before>import itertools
import threading
import datetime
import graphics
import random
import driver
import flask
from flask import request
import game
import time
import sys
from wsgiref import simple_server

class Message:
    def __init__(self, text, priority=5, expiration=None, effects=[]):
        self.text = text

        self.label = graphics.TextSprite(text, width=5, height=7, y=4)
        self.label.x = (112 - self.label.size()) // 2

        self.priority = priority
        self.expiration = expiration or datetime.datetime.max.timestamp()
        self.effects = []

        for effect_type in effects:
            effect = None
            if effect_type == "scroll":
                self.label.x = 112
                effect = graphics.Animator(self.label, attr="x", max=112,
                                           min=-self.label.size(),
                                           loop=True, delay=.04, step=-1)
            elif effect_type == "scroll_y":
                effect = graphics.Animator(self.label, attr="y", max=15,
                                           min=-self.label.height,
                                           loop=True, delay=.4)
            elif effect_type == "bounce_x":
                self.label.x = 112
                effect = graphics.Animator(
                    self.label, attr="x",
                    min=(112 - self.label.size() if self.label.size() > 112 else 0),
                    max=(0 if self.label.size() > 112 else 112 - self.label.size()),
                    delay=.04,
                    reverse=True)
            elif effect_type == "bounce_y":
                effect = graphics.Animator(self.label, attr="y",
                                           max=15-self.label.height, min=0,
                                           reverse=True, delay=.4)
            elif effect_type == "blink":
                effect = graphics.Animator(self.label, attr="visible", max=1, min=0,
                                           reverse=True, delay=1.5)
            elif effect_type == "blink_fast":
                effect = graphics.Animator(self.label, attr="visible", max=1, min=0,
                                           reverse=True, delay=.25)
            elif effect_type == "shake":
                effect = graphics.Animator(self.label, attr="y", max=6, min=2,
                                           delay=.02, reverse=True)

            if effect:
                self.effects.append(effect)

class MessageBoard(game.Game):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.messages = {}

        modes = [
            self.display_messages
        ]

        self.frame_lock = threading.Lock()

        self.api = flask.Flask(__name__)
        self.api.debug = True

        self.api.add_url_rule('/add_message', 'add_message', self.add_message, methods=['POST'])
        self.api.add_url_rule('/remove_message/<id>', 'remove_message', self.remove_message, methods=['GET', 'POST'])

        self.server = simple_server.make_server('', 8800, self.api)

        self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
        self.server_thread.start()

        self.ids = 0

        self.cycle = itertools.chain.from_iterable(
            mode() for mode in itertools.chain.from_iterable(
                random.shuffle(x) or x for x in itertools.repeat(modes)))

    def stop(self):
        super().stop()
        self.server.shutdown()
        self.server.server_close()

    def add_message(self):
        text = request.form.get("text", "?")
        priority = int(request.form.get("priority", 5))
        expiration = request.form.get("expiration", None)
        effects = filter(bool, request.form.get("effects", "").split(","))
        name = request.form.get("name", None)

        if name is None:
            name = str(self.ids)
            self.ids += 1

        with self.frame_lock:
            self.messages[name] = Message(text, priority, expiration, effects)

        return name

    def remove_message(self, id):
        with self.frame_lock:
            del self.messages[id]
        return ''

    def loop(self):
        super().loop()

        next(self.cycle)

    def display_messages(self):
        messages = None
        with self.frame_lock:
            messages = list(self.messages.values())

        for message in messages:
            self.sprites.add(message.label)
            for effect in message.effects:
                self.sprites.add(effect)

            run_until = time.time() + message.priority

            while time.time() < run_until:
                yield

            for effect in message.effects:
                self.sprites.remove(effect)
            self.sprites.remove(message.label)

        with self.frame_lock:
            self.messages = {k: m for k, m in self.messages.items() if m.expiration > time.time()}

GAME = MessageBoard
<commit_msg>Fix overflow<commit_after>import itertools
import threading
import datetime
import graphics
import random
import driver
import flask
from flask import request
import game
import time
import sys
from wsgiref import simple_server

class Message:
    def __init__(self, text, priority=5, expiration=None, effects=[]):
        self.text = text

        self.label = graphics.TextSprite(text, width=5, height=7, y=4)
        self.label.x = (112 - self.label.size()) // 2

        self.priority = priority
        self.expiration = expiration or 2147483647
        self.effects = []

        for effect_type in effects:
            effect = None
            if effect_type == "scroll":
                self.label.x = 112
                effect = graphics.Animator(self.label, attr="x", max=112,
                                           min=-self.label.size(),
                                           loop=True, delay=.04, step=-1)
            elif effect_type == "scroll_y":
                effect = graphics.Animator(self.label, attr="y", max=15,
                                           min=-self.label.height,
                                           loop=True, delay=.4)
            elif effect_type == "bounce_x":
                self.label.x = 112
                effect = graphics.Animator(
                    self.label, attr="x",
                    min=(112 - self.label.size() if self.label.size() > 112 else 0),
                    max=(0 if self.label.size() > 112 else 112 - self.label.size()),
                    delay=.04,
                    reverse=True)
            elif effect_type == "bounce_y":
                effect = graphics.Animator(self.label, attr="y",
                                           max=15-self.label.height, min=0,
                                           reverse=True, delay=.4)
            elif effect_type == "blink":
                effect = graphics.Animator(self.label, attr="visible", max=1, min=0,
                                           reverse=True, delay=1.5)
            elif effect_type == "blink_fast":
                effect = graphics.Animator(self.label, attr="visible", max=1, min=0,
                                           reverse=True, delay=.25)
            elif effect_type == "shake":
                effect = graphics.Animator(self.label, attr="y", max=6, min=2,
                                           delay=.02, reverse=True)

            if effect:
                self.effects.append(effect)

class MessageBoard(game.Game):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.messages = {}

        modes = [
            self.display_messages
        ]

        self.frame_lock = threading.Lock()

        self.api = flask.Flask(__name__)
        self.api.debug = True

        self.api.add_url_rule('/add_message', 'add_message', self.add_message, methods=['POST'])
        self.api.add_url_rule('/remove_message/<id>', 'remove_message', self.remove_message, methods=['GET', 'POST'])

        self.server = simple_server.make_server('', 8800, self.api)

        self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
        self.server_thread.start()

        self.ids = 0

        self.cycle = itertools.chain.from_iterable(
            mode() for mode in itertools.chain.from_iterable(
                random.shuffle(x) or x for x in itertools.repeat(modes)))

    def stop(self):
        super().stop()
        self.server.shutdown()
        self.server.server_close()

    def add_message(self):
        text = request.form.get("text", "?")
        priority = int(request.form.get("priority", 5))
        expiration = request.form.get("expiration", None)
        effects = filter(bool, request.form.get("effects", "").split(","))
        name = request.form.get("name", None)

        if name is None:
            name = str(self.ids)
            self.ids += 1

        with self.frame_lock:
            self.messages[name] = Message(text, priority, expiration, effects)

        return name

    def remove_message(self, id):
        with self.frame_lock:
            del self.messages[id]
        return ''

    def loop(self):
        super().loop()

        next(self.cycle)

    def display_messages(self):
        messages = None
        with self.frame_lock:
            messages = list(self.messages.values())

        for message in messages:
            self.sprites.add(message.label)
            for effect in message.effects:
                self.sprites.add(effect)

            run_until = time.time() + message.priority

            while time.time() < run_until:
                yield

            for effect in message.effects:
                self.sprites.remove(effect)
            self.sprites.remove(message.label)

        with self.frame_lock:
            self.messages = {k: m for k, m in self.messages.items() if m.expiration > time.time()}

GAME = MessageBoard
<|endoftext|>


<commit_before>import sys
import serial
import time
import math
import struct

response_wait = 0.01
global s, verbose_mode, cmd, max_command_chars
s = None
verbose_mode = False
cmd = ""
max_command_chars = None

def begin(verbose=False):
    global s, verbose_mode, max_command_chars
    s = serial.Serial("/dev/ttyS0", 115200)
    verbose_mode = verbose
    flush_output()
    flush_input()
    max_command_chars = get_max_string_length()
    if max_command_chars == None or max_command_chars == 0:
        #raise StandardError("unable to get maximum command chars")
        # just use a default
        max_command_chars = 60
    max_command_chars -= 1

def flush_input():
    s.flushInput()

def flush_output():
    s.flushOutput()

def wait_for_ack():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    while s.inWaiting() > 0:
        s.read(s.inWaiting())
#        print s.read(s.inWaiting()),
#    print

def wait_for_int():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    intstr = ""
    while s.inWaiting() > 0:
        intstr = intstr + s.read(s.inWaiting())
    try:
        return int(intstr[:-1])
    except ValueError:
        print "whoops " + intstr
        return 0

def wait_for_str():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    str = ""
    while s.inWaiting() > 0:
        str = str + s.read(s.inWaiting())
    return str[:-1]

def send_command(cmd_text):
    if verbose_mode:
        print "sending: " + cmd_text
    s.write((cmd_text + ':\0:').encode())

def command(cmd_text):
    send_command(cmd_text)
    wait_for_ack()

def command_int(cmd_text):
    send_command(cmd_text)
    return wait_for_int()

def command_str(cmd_text):
    send_command(cmd_text)
    return wait_for_str()

def write(text):
    sys.stdout.write(text)
    sys.stdout.flush()

def get_device_config(config):
    return command_int("0," + str(config) + ":tst")

def get_num_leds():
    return get_device_config(0)

def get_palette_size():
    return get_device_config(1)

def get_offset():
    return get_device_config(2)

def get_window():
    return get_device_config(3)

def get_default_brightness():
    return get_device_config(4)

def get_minimum_brightness():
    return get_device_config(5)

def get_reverse():
    return get_device_config(6)

def get_default_fade_rate():
    return get_device_config(7)

def get_fade_rate():
    return get_device_config(8)

def get_mapping_enabled():
    return get_device_config(9)

def get_default_lightness():
    return get_device_config(10)

def get_minimum_lightness():
    return get_device_config(11)

def get_max_string_length():
    return get_device_config(12)

def get_test_framework_enabled():
    return get_device_config(13)

def get_extra_shuffles_enabled():
    return get_device_config(14)

def get_blend_enabled():
    return get_device_config(15)

def get_num_macro_chars():
    return get_device_config(16)

def get_num_memory_macros():
    return get_device_config(17)

def get_num_eeprom_macros():
    return get_device_config(18)

def get_first_eeprom_macro():
    return get_device_config(19)

def get_last_eeprom_macro():
    return get_first_eeprom_macro() + int(math.ceil(1024.0 / get_num_macro_chars()) - 1);

def get_num_sequencers():
    return get_device_config(20)

def get_num_fine_zones():
    return get_device_config(21)

# device parameteruseful for compiling and device programming
def get_device_profile():
    return {
        "CHAR-BUFFER-SIZE": get_max_string_length(),
        "NUM-LEDS": get_num_leds(),
        "NUM-MACROS": get_num_eeprom_macros(),
        "NUM-MACRO-CHARS": get_num_macro_chars(),
        "NUM-SEQUENCERS": get_num_sequencers(),
        "NUM-FINE-ZONES": get_num_fine_zones(),
        "NUM-PALETTE-COLORS": get_palette_size(),
        "START-MACRO": get_first_eeprom_macro(),
        "END-MACRO": get_last_eeprom_macro(),
        "CHAR-BUFFER-SIZE": get_max_string_length()
    }

def push_command(cmd_text=None):
    global cmd

    # without argument, send accumulated command if any
    if cmd_text == None:
        if len(cmd) > 0:
            command(cmd)
            cmd = ""
        return

    if len(cmd_text) > max_command_chars:
        raise StandardError("command length exceeds maximum. command: " + cmd_text + "length: " + str(len(cmd_text)) + " max length: " + str(max_command_chars))

    if len(cmd) + len(cmd_text) > max_command_chars:
        # adding new comand text would exceed maximum
        # send the accumulated commands and clear the buffer
        command(cmd)
        cmd = cmd_text
    else:
        # add this command text to the buffer
        cmd = cmd + cmd_text

def pause():
  command("3:pau")

def resume():
  command("3:cnt")

def attention(erase=True):
    command("::pau")
    if erase:
        command("::pau:era:flu")
    else:
        command("::pau")

def set_macro(macro, macro_text, expected_bytes, debug_mode):
    if debug_mode:
      print "macro " + str(macro) + ": ",
    bytes = command_int(str(macro) + ":set:" + macro_text)
    if debug_mode:
        #print str(bytes) + " bytes"
        macro_bytes = command_str("1," + str(macro) + ":tst")
        #print "Macro bytes: " + str(macro_bytes)
        print "Macro: " + lc.get_macro(macro)
    else:
        if expected_bytes > 0 and expected_bytes != bytes:
            raise StandardError("Wrong number of bytes received for macro %s" % str(macro))
    return bytes

def run_macro(macro):
    command("1:pau:2:cnt:%s:run" % macro)

def get_macro(macro):
    macro_bytes = get_full_macro_bytes(macro)
    return translate_macro_bytes(macro_bytes)

def get_macro_bytes(macro):
    bytes_string = command_str("1," + str(macro) + ":tst")[:-1]
    byte_strings = bytes_string.split(",")
    result = []
    for byte_string in byte_strings:
      #print "byte string: " + byte_string
      result.append(int(byte_string))
    return result

def get_full_macro_bytes(macro):
  result = []
  buffer = []
  macro_size = get_num_macro_chars()
  last_macro = get_last_eeprom_macro()
  current_macro = macro
  index = 0
  script_terminator_found = False
  macro_bytes_max = 0
  while True:
    if current_macro > last_macro:
      raise ValueError("Ran out of macro numbers getting a consolidated macro")
    macro_bytes = get_macro_bytes(current_macro)
    buffer += macro_bytes
    macro_bytes_max += macro_size
    #print str(macro_bytes)
    #index = 0
    #script_terminator_found = False
    while(index < macro_bytes_max):
      #print "index: " + str(index)
      #print "len: " + str(len(macro_bytes))
      #print "max: " + str(macro_bytes_max)
      byte = buffer[index]
      if byte == 255 or byte == 0:
        script_terminator_found = True
        break
      if is_arg_marker(byte):
        format, size = get_arg_decode_info(byte)
        # index could be the last byte in the buffer
        # at least one more byte is needed
        if index + size >= macro_bytes_max:
          # break the inner loop to get the next set of bytes
          # and don't increment the index so the byte is checked again
          break
        result.append(byte)
        index += 1
        for i in range(0, size):
          #print "INDEX: " + str(index)
          byte = buffer[index]
          result.append(byte)
          index += 1
      else:
        result.append(byte)
        index += 1
    if script_terminator_found:
      break;
    current_macro += 1
  return result

def stop_all():
    attention()
    command("stp")

#define MACRO_END_MARKER 0xff
#define MACRO_ARG1_MARKER 0xf9
#define MACRO_ARG2_MARKER 0xfa
#define MACRO_ARG3_MARKER 0xfb
#define MACRO_ARG4_MARKER 0xfc
#define MACRO_ARG5_MARKER 0xfd
#define MACRO_ARG6_MARKER 0xfe

def is_arg_marker(byte):
  return byte >= 240 and byte <= 254

def is_macro_end_marker(byte):
  return byte == 255

def get_arg_decode_info(marker):
  decode_args = {
    249: { "format": "B", "size": 1 },
    250: { "format": "h", "size": 2 },
    251: { "format": "hB", "size": 3 },
    252: { "format": "hh", "size": 4 },
    253: { "format": "hhB", "size": 5 },
    254: { "format": "hhh", "size": 6}
  }
  format = decode_args[marker]["format"]
  size = decode_args[marker]["size"]
  return format, size

def decode_args(bytes):
  leftover_bytes = []
  #print "bytes: " + str(bytes) #@@@

  marker = bytes[0]
  format, size = get_arg_decode_info(marker)
  remaining_bytes = bytes[1:]
  #print "remaining bytes: " + str(remaining_bytes)
  value_bytes = remaining_bytes[:size]
  #print "value bytes: " + str(value_bytes)
  packed_as_bytes = struct.pack(str(size) + "B", *tuple(value_bytes))
  values = struct.unpack(">" + format, packed_as_bytes)
  #print "values: " + str(values)
  args_string = ""
  for value in values:
    args_string += str(value) + ","
  return args_string[:-1], size

def translate_macro_bytes(macro_bytes):
    commands_cutoff = 240
    macro = []
    index = 0
    while index < len(macro_bytes):
        byte = macro_bytes[index]
        if byte == 255:
          break;
        if is_arg_marker(byte):
          args_string, size = decode_args(macro_bytes[index:])
          macro.append(args_string)
          index += size
        else:
          macro.append(lookup_command(byte))
        index += 1
    return ":".join(macro)

def lookup_command(byte):
  return commands()[byte]

def commands():
    return {
        1: "flu",
        2: "rst",
        3: "era",
        4: "rot",
        5: "rep",
        6: "cpy",
        7: "flo",
        8: "mir",
        9: "dis",
        10: "zon",
        11: "win",
        12: "off",
        13: "rev",
        14: "rgb",
        15: "hsl",
        16: "red",
        17: "org",
        18: "yel",
        19: "grn",
        20: "blu",
        21: "pur",
        22: "cyn",
        23: "mag",
        24: "lbl",
        25: "lgr",
        26: "sea",
        27: "pnk",
        28: "amb",
        29: "olv",
        30: "sky",
        31: "tur",
        32: "lav",
        33: "ros",
        34: "dgr",
        35: "gry",
        36: "wht",
        37: "tun",
        38: "neo",
        39: "sod",
        40: "blk",
        41: "rnd",
        42: "dyn",
        43: "ble",
        44: "max",
        45: "dim",
        46: "brt",
        47: "sta",
        48: "bli",
        49: "bl1",
        50: "bl2",
        51: "bl3",
        52: "bl4",
        53: "bl5",
        54: "bl6",
        55: "bla",
        56: "blb",
        57: "bld",
        58: "bre",
        59: "brd",
        60: "sfd",
        61: "ffd",
        62: "sto",
        63: "rcl",
        64: "psh",
        65: "pau",
        66: "cnt",
        67: "clr",
        68: "lev",
        69: "fad",
        70: "art",
        71: "cfa",
        72: "blr",
        73: "efr",
        74: "tst",
        75: "cfg",
        76: "pin",
        77: "sch",
        78: "car",
        79: "set",
        80: "run",
        81: "del",
        82: "stp",
        83: "rng",
        84: "pos",
        85: "rps",
        86: "xyp",
        87: "pal",
        88: "shf",
        89: "sbl",
        90: "seq",
        91: "sqs",
        92: "swc",
        93: "sws",
        94: "snw",
        95: "csh",
        96: "css",
        97: "csl",
        98: "fan",
        99: "app",
        100: "add",
        101: "sub",
        102: "mul",
        103: "div",
        104: "mod",
        105: "dif",
        106: "avg",
        107: "drw"
    }

<commit_msg>argument decoding is working<commit_after>import sys
import serial
import time
import math
import struct

response_wait = 0.01
global s, verbose_mode, cmd, max_command_chars
s = None
verbose_mode = False
cmd = ""
max_command_chars = None

def begin(verbose=False):
    global s, verbose_mode, max_command_chars
    s = serial.Serial("/dev/ttyS0", 115200)
    verbose_mode = verbose
    flush_output()
    flush_input()
    max_command_chars = get_max_string_length()
    if max_command_chars == None or max_command_chars == 0:
        #raise StandardError("unable to get maximum command chars")
        # just use a default
        max_command_chars = 60
    max_command_chars -= 1

def flush_input():
    s.flushInput()

def flush_output():
    s.flushOutput()

def wait_for_ack():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    while s.inWaiting() > 0:
        s.read(s.inWaiting())
#        print s.read(s.inWaiting()),
#    print

def wait_for_int():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    intstr = ""
    while s.inWaiting() > 0:
        intstr = intstr + s.read(s.inWaiting())
    try:
        return int(intstr[:-1])
    except ValueError:
        print "whoops " + intstr
        return 0

def wait_for_str():
    while s.inWaiting() <= 0:
        pass
    time.sleep(response_wait);
    str = ""
    while s.inWaiting() > 0:
        str = str + s.read(s.inWaiting())
    return str[:-1]

def send_command(cmd_text):
    if verbose_mode:
        print "sending: " + cmd_text
    s.write((cmd_text + ':\0:').encode())

def command(cmd_text):
    send_command(cmd_text)
    wait_for_ack()

def command_int(cmd_text):
    send_command(cmd_text)
    return wait_for_int()

def command_str(cmd_text):
    send_command(cmd_text)
    return wait_for_str()

def write(text):
    sys.stdout.write(text)
    sys.stdout.flush()

def get_device_config(config):
    return command_int("0," + str(config) + ":tst")

def get_num_leds():
    return get_device_config(0)

def get_palette_size():
    return get_device_config(1)

def get_offset():
    return get_device_config(2)

def get_window():
    return get_device_config(3)

def get_default_brightness():
    return get_device_config(4)

def get_minimum_brightness():
    return get_device_config(5)

def get_reverse():
    return get_device_config(6)

def get_default_fade_rate():
    return get_device_config(7)

def get_fade_rate():
    return get_device_config(8)

def get_mapping_enabled():
    return get_device_config(9)

def get_default_lightness():
    return get_device_config(10)

def get_minimum_lightness():
    return get_device_config(11)

def get_max_string_length():
    return get_device_config(12)

def get_test_framework_enabled():
    return get_device_config(13)

def get_extra_shuffles_enabled():
    return get_device_config(14)

def get_blend_enabled():
    return get_device_config(15)

def get_num_macro_chars():
    return get_device_config(16)

def get_num_memory_macros():
    return get_device_config(17)

def get_num_eeprom_macros():
    return get_device_config(18)

def get_first_eeprom_macro():
    return get_device_config(19)

def get_last_eeprom_macro():
    return get_first_eeprom_macro() + int(math.ceil(1024.0 / get_num_macro_chars()) - 1);

def get_num_sequencers():
    return get_device_config(20)

def get_num_fine_zones():
    return get_device_config(21)

# device parameteruseful for compiling and device programming
def get_device_profile():
    return {
        "CHAR-BUFFER-SIZE": get_max_string_length(),
        "NUM-LEDS": get_num_leds(),
        "NUM-MACROS": get_num_eeprom_macros(),
        "NUM-MACRO-CHARS": get_num_macro_chars(),
        "NUM-SEQUENCERS": get_num_sequencers(),
        "NUM-FINE-ZONES": get_num_fine_zones(),
        "NUM-PALETTE-COLORS": get_palette_size(),
        "START-MACRO": get_first_eeprom_macro(),
        "END-MACRO": get_last_eeprom_macro(),
        "CHAR-BUFFER-SIZE": get_max_string_length()
    }

def push_command(cmd_text=None):
    global cmd

    # without argument, send accumulated command if any
    if cmd_text == None:
        if len(cmd) > 0:
            command(cmd)
            cmd = ""
        return

    if len(cmd_text) > max_command_chars:
        raise StandardError("command length exceeds maximum. command: " + cmd_text + "length: " + str(len(cmd_text)) + " max length: " + str(max_command_chars))

    if len(cmd) + len(cmd_text) > max_command_chars:
        # adding new comand text would exceed maximum
        # send the accumulated commands and clear the buffer
        command(cmd)
        cmd = cmd_text
    else:
        # add this command text to the buffer
        cmd = cmd + cmd_text

def pause():
  command("3:pau")

def resume():
  command("3:cnt")

def attention(erase=True):
    command("::pau")
    if erase:
        command("::pau:era:flu")
    else:
        command("::pau")

def set_macro(macro, macro_text, expected_bytes, debug_mode):
    if debug_mode:
      print "macro " + str(macro) + ": ",
    bytes = command_int(str(macro) + ":set:" + macro_text)
    if debug_mode:
        #print str(bytes) + " bytes"
        macro_bytes = command_str("1," + str(macro) + ":tst")
        #print "Macro bytes: " + str(macro_bytes)
        print "Macro: " + lc.get_macro(macro)
    else:
        if expected_bytes > 0 and expected_bytes != bytes:
            raise StandardError("Wrong number of bytes received for macro %s" % str(macro))
    return bytes

def run_macro(macro):
    command("1:pau:2:cnt:%s:run" % macro)

def get_macro(macro):
    macro_bytes = get_full_macro_bytes(macro)
    return str(macro) + ":set:" + translate_macro_bytes(macro_bytes)

def get_macro_bytes(macro):
    bytes_string = command_str("1," + str(macro) + ":tst")[:-1]
    byte_strings = bytes_string.split(",")
    result = []
    for byte_string in byte_strings:
      #print "byte string: " + byte_string
      result.append(int(byte_string))
    return result

def get_full_macro_bytes(macro):
  result = []
  buffer = []
  macro_size = get_num_macro_chars()
  last_macro = get_last_eeprom_macro()
  current_macro = macro
  index = 0
  script_terminator_found = False
  macro_bytes_max = 0
  while True:
    if current_macro > last_macro:
      raise ValueError("Ran out of macro numbers getting a consolidated macro")
    macro_bytes = get_macro_bytes(current_macro)
    buffer += macro_bytes
    macro_bytes_max += macro_size
    #print str(macro_bytes)
    #index = 0
    #script_terminator_found = False
    while(index < macro_bytes_max):
      #print "index: " + str(index)
      #print "len: " + str(len(macro_bytes))
      #print "max: " + str(macro_bytes_max)
      byte = buffer[index]
      if byte == 255 or byte == 0:
        script_terminator_found = True
        break
      if is_arg_marker(byte):
        format, size = get_arg_decode_info(byte)
        # index could be the last byte in the buffer
        # at least one more byte is needed
        if index + size >= macro_bytes_max:
          # break the inner loop to get the next set of bytes
          # and don't increment the index so the byte is checked again
          break
        result.append(byte)
        index += 1
        for i in range(0, size):
          #print "INDEX: " + str(index)
          byte = buffer[index]
          result.append(byte)
          index += 1
      else:
        result.append(byte)
        index += 1
    if script_terminator_found:
      break;
    current_macro += 1
  return result

def stop_all():
    attention()
    command("stp")

#define MACRO_END_MARKER 0xff
#define MACRO_ARG1_MARKER 0xf9
#define MACRO_ARG2_MARKER 0xfa
#define MACRO_ARG3_MARKER 0xfb
#define MACRO_ARG4_MARKER 0xfc
#define MACRO_ARG5_MARKER 0xfd
#define MACRO_ARG6_MARKER 0xfe

def is_arg_marker(byte):
  return byte >= 240 and byte <= 254

def is_macro_end_marker(byte):
  return byte == 255

def get_arg_decode_info(marker):
  decode_args = {
    249: { "format": "B", "size": 1 },
    250: { "format": "h", "size": 2 },
    251: { "format": "hB", "size": 3 },
    252: { "format": "hh", "size": 4 },
    253: { "format": "hhB", "size": 5 },
    254: { "format": "hhh", "size": 6}
  }
  format = decode_args[marker]["format"]
  size = decode_args[marker]["size"]
  return format, size

def decode_args(bytes):
  leftover_bytes = []
  #print "bytes: " + str(bytes) #@@@

  marker = bytes[0]
  format, size = get_arg_decode_info(marker)
  remaining_bytes = bytes[1:]
  #print "remaining bytes: " + str(remaining_bytes)
  value_bytes = remaining_bytes[:size]
  #print "value bytes: " + str(value_bytes)
  packed_as_bytes = struct.pack(str(size) + "B", *tuple(value_bytes))
  values = struct.unpack(format, packed_as_bytes)
  #print "values: " + str(values)
  args_string = ""
  for value in values:
    args_string += str(value) + ","
  return args_string[:-1], size

def translate_macro_bytes(macro_bytes):
    commands_cutoff = 240
    macro = []
    index = 0
    while index < len(macro_bytes):
        byte = macro_bytes[index]
        if byte == 255:
          break;
        if is_arg_marker(byte):
          args_string, size = decode_args(macro_bytes[index:])
          macro.append(args_string)
          index += size
        else:
          macro.append(lookup_command(byte))
        index += 1
    return ":".join(macro)

def lookup_command(byte):
  return commands()[byte]

def commands():
    return {
        1: "flu",
        2: "rst",
        3: "era",
        4: "rot",
        5: "rep",
        6: "cpy",
        7: "flo",
        8: "mir",
        9: "dis",
        10: "zon",
        11: "win",
        12: "off",
        13: "rev",
        14: "rgb",
        15: "hsl",
        16: "red",
        17: "org",
        18: "yel",
        19: "grn",
        20: "blu",
        21: "pur",
        22: "cyn",
        23: "mag",
        24: "lbl",
        25: "lgr",
        26: "sea",
        27: "pnk",
        28: "amb",
        29: "olv",
        30: "sky",
        31: "tur",
        32: "lav",
        33: "ros",
        34: "dgr",
        35: "gry",
        36: "wht",
        37: "tun",
        38: "neo",
        39: "sod",
        40: "blk",
        41: "rnd",
        42: "dyn",
        43: "ble",
        44: "max",
        45: "dim",
        46: "brt",
        47: "sta",
        48: "bli",
        49: "bl1",
        50: "bl2",
        51: "bl3",
        52: "bl4",
        53: "bl5",
        54: "bl6",
        55: "bla",
        56: "blb",
        57: "bld",
        58: "bre",
        59: "brd",
        60: "sfd",
        61: "ffd",
        62: "sto",
        63: "rcl",
        64: "psh",
        65: "pau",
        66: "cnt",
        67: "clr",
        68: "lev",
        69: "fad",
        70: "art",
        71: "cfa",
        72: "blr",
        73: "efr",
        74: "tst",
        75: "cfg",
        76: "pin",
        77: "sch",
        78: "car",
        79: "set",
        80: "run",
        81: "del",
        82: "stp",
        83: "rng",
        84: "pos",
        85: "rps",
        86: "xyp",
        87: "pal",
        88: "shf",
        89: "sbl",
        90: "seq",
        91: "sqs",
        92: "swc",
        93: "sws",
        94: "snw",
        95: "csh",
        96: "css",
        97: "csl",
        98: "fan",
        99: "app",
        100: "add",
        101: "sub",
        102: "mul",
        103: "div",
        104: "mod",
        105: "dif",
        106: "avg",
        107: "drw"
    }

<|endoftext|>


<commit_before>import mido
from Queue import Queue
import music
from threading import Thread
import logging
import signal
from datetime import datetime

REFACE = 'reface DX'
MPX = 'MPX16'
SIMPLE_SYNTH = 'SimpleSynth virtual input'

CHANNEL_PARTITION = 8

simple_port = None

# noinspection PyBroadException
try:
    instrument = music.MidiInstrument()
except Exception:
    logging.warn("Midi instrument could not be opened")

# Creates a port object corresponding to an instrument if it exists, else to a Simple inbuilt synth
def make_port(name):
    global simple_port
    try:
        # noinspection PyUnresolvedReferences
        return mido.open_output(name)
    except IOError:
        logging.warn("{} not found.".format(name))
        if simple_port is None:
            # noinspection PyUnresolvedReferences
            simple_port = mido.open_output(SIMPLE_SYNTH)
        return simple_port

keys_port = make_port(REFACE)
drum_port = make_port(MPX)

# Class representing key-value pairs that can be queued as commands
class Command:
    def __init__(self, name, value=None):
        self.name = name
        self.value = value

    pitch_bend = "pitch_bend"
    volume = "volume"
    fade_out = "fade_out"

# Class representing individual midi channel
class Channel:
    note_off = "note_off"
    note_on = "note_on"

    def __init__(self, number, volume=1.0, fade_rate=0.1, note_on_listener=None):
        self.note_on_listener = note_on_listener
        self.number = number
        self.port = keys_port if number < CHANNEL_PARTITION else drum_port
        self.volume = volume
        self.fade_rate = fade_rate
        self.queue = Queue()
        self.fade_start = None
        self.playing_notes = []

    # Send a midi message to this channel
    def send_message(self, msg):
        try:
            # Are there any commands waiting?
            if not self.queue.empty():
                command = self.queue.get()
                # Volume changing command. Overrides current fades
                if command.name == Command.volume:
                    self.volume = command.value
                    self.fade_start = None
                # Fade out command. Fade out occurs constantly until silent or overridden by volume change
                elif command.name == Command.fade_out:
                    # Fade out command has no effect if fadeout already underway
                    if self.fade_start is None:
                        # When did the fadeout start?
                        self.fade_start = datetime.now()
            # True if a fade out is in progress
            if self.fade_start is not None:
                # How long has the fade been occurring?
                seconds = (datetime.now() - self.fade_start).total_seconds()
                self.volume *= (1 - self.fade_rate * seconds)
                if self.volume < 0:
                    self.volume = 0
                    self.fade_start = None
            msg.velocity = int(self.volume * msg.velocity)
            # Actually send the midi message
            self.port.send(msg)
            # Check if it was a note message
            if msg.type == Channel.note_on:
                # Keep track of notes that are currently playing
                self.playing_notes.append(msg.note)
                self.playing_notes.sort()
                if self.note_on_listener is not None:
                    self.note_on_listener(msg)
            elif msg.type == Channel.note_off:
                self.playing_notes.remove(msg.note)
        except AttributeError:
            pass

    # Stop all currently playing notes
    def stop_playing_notes(self):
        for note in self.playing_notes:
            self.stop_note(note)

    # Stop a specific note
    def stop_note(self, note):
        # noinspection PyTypeChecker
        self.send_message(mido.Message(type=Channel.note_off, velocity=0, note=note))

    # Play a specific note
    def play_note(self, note, velocity=100):
        # noinspection PyTypeChecker
        self.send_message(mido.Message(type=Channel.note_on, velocity=velocity, note=note))

    # Returns a currently playing note for a position or the last playing note if the position too high. Throws index
    # error if there are no playing notes
    def playing_note_with_position(self, position):
        if position > len(self.playing_notes):
            position = len(self.playing_notes) - 1
        return self.playing_notes[position]

    # Set the volume of this channel (float from 0 - 1)
    def set_volume(self, volume):
        self.queue.put(Command(Command.volume, volume))

    # Start fading out this channel
    def fade_out(self):
        self.queue.put(Command(Command.fade_out))

# A bass channel that plays a note from a chord shifted down two octaves on a drum beat
class BassChannel(Channel, object):
    def __init__(self, number, chord_channel, drum_channel, volume=1.0, octave_shift=2):
        super(BassChannel, self).__init__(number, volume)
        self.chord_channel = chord_channel
        self.drum_channel = drum_channel
        self.drum_channel.note_on_listener = self.on_drum_played
        self.pressed_positions = []
        self.octave_shift = octave_shift
        self.pressed_positions_queue = Queue()

    # Called when a drum beat is played
    def on_drum_played(self, msg):
        # Stop all playing notes
        self.stop_playing_notes()
        # Updated pressed positions if waiting in queue
        if not self.pressed_positions_queue.empty():
            self.pressed_positions = self.pressed_positions_queue.get()
        for position in self.pressed_positions:
            try:
                # Grab a note from the chord channel for each pressed position
                note = self.chord_channel.playing_note_with_position(position)

                msg.channel = self.number
                msg.note = note - 12 * self.octave_shift

                self.send_message(msg)
            except IndexError as e:
                logging.exception(e)

    # Set the positions of pressed notes
    def set_pressed_positions(self, pressed_positions):
        self.pressed_positions_queue.put(pressed_positions)

# Represents a midi song loaded from a file
class Song:
    def __init__(self, filename="channels_test.mid", is_looping=False):
        self.filename = filename
        self.queue = Queue()
        self.is_stopping = False
        self.is_looping = is_looping
        self.mid = mido.MidiFile("media/{}".format(self.filename))
        signal.signal(signal.SIGINT, self.stop)
        channel_numbers = range(0, 16)
        self.channels = map(Channel, sorted(list(channel_numbers)))

    # Play the midi file (should be called on new thread)
    def play_midi_file(self):
        self.is_stopping = False

        # Take midi messages from a generator
        for msg in self.mid.play():
            # Break if should stop
            if self.is_stopping:
                break
            # Check if any commands in queue
            if not self.queue.empty():
                command = self.queue.get()
                if isinstance(command, Command):
                    if command.name == Command.pitch_bend:
                        instrument.pitch_bend(command.value)
            try:
                # Send a message to its assigned channel
                self.channels[msg.channel].send_message(msg)
            except AttributeError as e:
                logging.exception(e)
            except IndexError as e:
                logging.exception(e)
        # Loop again if set to do so
        if self.is_looping and not self.is_stopping:
            self.play_midi_file()

    # Start this song on a new thread
    def start(self):
        t = Thread(target=self.play_midi_file)
        t.start()

    # Stop this song
    def stop(self):
        self.is_stopping = True

    # Queue up a command
    def send_command(self, name, value):
        self.queue.put(Command(name, value))

    # Set which channels should be playing
    def set_included_channels(self, pressed_positions):
        for channel in self.channels:
            if channel.number in pressed_positions:
                channel.set_volume(1.0)
            else:
                channel.fade_out()

    # Play all channels
    def include_all_channels(self):
        for channel in self.channels:
            channel.set_volume(1.0)
<commit_msg>octaving bass note when out of list range<commit_after>import mido
from Queue import Queue
import music
from threading import Thread
import logging
import signal
from datetime import datetime

REFACE = 'reface DX'
MPX = 'MPX16'
SIMPLE_SYNTH = 'SimpleSynth virtual input'

CHANNEL_PARTITION = 8

simple_port = None

# noinspection PyBroadException
try:
    instrument = music.MidiInstrument()
except Exception:
    logging.warn("Midi instrument could not be opened")

# Creates a port object corresponding to an instrument if it exists, else to a Simple inbuilt synth
def make_port(name):
    global simple_port
    try:
        # noinspection PyUnresolvedReferences
        return mido.open_output(name)
    except IOError:
        logging.warn("{} not found.".format(name))
        if simple_port is None:
            # noinspection PyUnresolvedReferences
            simple_port = mido.open_output(SIMPLE_SYNTH)
        return simple_port

keys_port = make_port(REFACE)
drum_port = make_port(MPX)

# Class representing key-value pairs that can be queued as commands
class Command:
    def __init__(self, name, value=None):
        self.name = name
        self.value = value

    pitch_bend = "pitch_bend"
    volume = "volume"
    fade_out = "fade_out"

# Class representing individual midi channel
class Channel:
    note_off = "note_off"
    note_on = "note_on"

    def __init__(self, number, volume=1.0, fade_rate=0.1, note_on_listener=None):
        self.note_on_listener = note_on_listener
        self.number = number
        self.port = keys_port if number < CHANNEL_PARTITION else drum_port
        self.volume = volume
        self.fade_rate = fade_rate
        self.queue = Queue()
        self.fade_start = None
        self.playing_notes = []

    # Send a midi message to this channel
    def send_message(self, msg):
        try:
            # Are there any commands waiting?
            if not self.queue.empty():
                command = self.queue.get()
                # Volume changing command. Overrides current fades
                if command.name == Command.volume:
                    self.volume = command.value
                    self.fade_start = None
                # Fade out command. Fade out occurs constantly until silent or overridden by volume change
                elif command.name == Command.fade_out:
                    # Fade out command has no effect if fadeout already underway
                    if self.fade_start is None:
                        # When did the fadeout start?
                        self.fade_start = datetime.now()
            # True if a fade out is in progress
            if self.fade_start is not None:
                # How long has the fade been occurring?
                seconds = (datetime.now() - self.fade_start).total_seconds()
                self.volume *= (1 - self.fade_rate * seconds)
                if self.volume < 0:
                    self.volume = 0
                    self.fade_start = None
            msg.velocity = int(self.volume * msg.velocity)
            # Actually send the midi message
            self.port.send(msg)
            # Check if it was a note message
            if msg.type == Channel.note_on:
                # Keep track of notes that are currently playing
                self.playing_notes.append(msg.note)
                self.playing_notes.sort()
                if self.note_on_listener is not None:
                    self.note_on_listener(msg)
            elif msg.type == Channel.note_off:
                self.playing_notes.remove(msg.note)
        except AttributeError:
            pass

    # Stop all currently playing notes
    def stop_playing_notes(self):
        for note in self.playing_notes:
            self.stop_note(note)

    # Stop a specific note
    def stop_note(self, note):
        # noinspection PyTypeChecker
        self.send_message(mido.Message(type=Channel.note_off, velocity=0, note=note))

    # Set the volume of this channel (float from 0 - 1)
    def set_volume(self, volume):
        self.queue.put(Command(Command.volume, volume))

    # Start fading out this channel
    def fade_out(self):
        self.queue.put(Command(Command.fade_out))

# A bass channel that plays a note from a chord shifted down two octaves on a drum beat
class BassChannel(Channel, object):
    def __init__(self, number, chord_channel, drum_channel, volume=1.0, octave_shift=2):
        super(BassChannel, self).__init__(number, volume)
        self.chord_channel = chord_channel
        self.drum_channel = drum_channel
        self.drum_channel.note_on_listener = self.on_drum_played
        self.pressed_positions = []
        self.octave_shift = octave_shift
        self.pressed_positions_queue = Queue()

    # Called when a drum beat is played
    def on_drum_played(self, msg):
        # Stop all playing notes
        self.stop_playing_notes()
        # Updated pressed positions if waiting in queue
        if not self.pressed_positions_queue.empty():
            self.pressed_positions = self.pressed_positions_queue.get()
        for position in self.pressed_positions:
            try:
                note = self.chord_channel.playing_notes[position % len(self.chord_channel.playing_notes)]
                shift = position / len(self.chord_channel.playing_notes)

                msg.channel = self.number
                msg.note = note + 12 * (shift - self.octave_shift)

                self.send_message(msg)
            except ZeroDivisionError as e:
                logging.exception(e)

    # Set the positions of pressed notes
    def set_pressed_positions(self, pressed_positions):
        self.pressed_positions_queue.put(pressed_positions)

# Represents a midi song loaded from a file
class Song:
    def __init__(self, filename="channels_test.mid", is_looping=False):
        self.filename = filename
        self.queue = Queue()
        self.is_stopping = False
        self.is_looping = is_looping
        self.mid = mido.MidiFile("media/{}".format(self.filename))
        signal.signal(signal.SIGINT, self.stop)
        channel_numbers = range(0, 16)
        self.channels = map(Channel, sorted(list(channel_numbers)))

    # Play the midi file (should be called on new thread)
    def play_midi_file(self):
        self.is_stopping = False

        # Take midi messages from a generator
        for msg in self.mid.play():
            # Break if should stop
            if self.is_stopping:
                break
            # Check if any commands in queue
            if not self.queue.empty():
                command = self.queue.get()
                if isinstance(command, Command):
                    if command.name == Command.pitch_bend:
                        instrument.pitch_bend(command.value)
            try:
                # Send a message to its assigned channel
                self.channels[msg.channel].send_message(msg)
            except AttributeError as e:
                logging.exception(e)
            except IndexError as e:
                logging.exception(e)
        # Loop again if set to do so
        if self.is_looping and not self.is_stopping:
            self.play_midi_file()

    # Start this song on a new thread
    def start(self):
        t = Thread(target=self.play_midi_file)
        t.start()

    # Stop this song
    def stop(self):
        self.is_stopping = True

    # Queue up a command
    def send_command(self, name, value):
        self.queue.put(Command(name, value))

    # Set which channels should be playing
    def set_included_channels(self, pressed_positions):
        for channel in self.channels:
            if channel.number in pressed_positions:
                channel.set_volume(1.0)
            else:
                channel.fade_out()

    # Play all channels
    def include_all_channels(self):
        for channel in self.channels:
            channel.set_volume(1.0)
<|endoftext|>


<commit_before>import mido
from Queue import Queue
import music
from threading import Thread
import logging
import signal
from datetime import datetime

# noinspection PyUnresolvedReferences
input_names = mido.get_output_names()

REFACE = 'reface DX'
MPX = 'MPX16'
SIMPLE_SYNTH = 'SimpleSynth virtual input'

CHANNEL_PARTITION = 8

simple_port = None

# noinspection PyBroadException
try:
    instrument = music.MidiInstrument()
except Exception:
    logging.warn("Midi instrument could not be opened")

# Creates a port object corresponding to an instrument if it exists, else to a Simple inbuilt synth
def make_port(name):
    for input_name in input_names:
        if name in input_name:
            name = input_name
    global simple_port
    try:
        # noinspection PyUnresolvedReferences
        return mido.open_output(name)
    except IOError:
        logging.warn("{} not found.".format(name))
        if simple_port is None:
            # noinspection PyUnresolvedReferences
            simple_port = mido.open_output()
        return simple_port

keys_port = make_port(REFACE)
drum_port = make_port(MPX)

# Class representing key-value pairs that can be queued as commands
class Command:
    def __init__(self, name, value=None):
        self.name = name
        self.value = value

    pitch_bend = "pitch_bend"
    volume = "volume"
    fade_out = "fade_out"

# Class representing individual midi channel
class Channel:
    note_off = "note_off"
    note_on = "note_on"

    def __init__(self, number, volume=1.0, fade_rate=0.1, note_on_listener=None):
        self.note_on_listener = note_on_listener
        self.number = number
        self.port = keys_port if number < CHANNEL_PARTITION else drum_port
        self.volume = volume
        self.fade_rate = fade_rate
        self.queue = Queue()
        self.fade_start = None
        self.playing_notes = []

    # Send a midi message to this channel
    def send_message(self, msg):
        try:
            # Are there any commands waiting?
            if not self.queue.empty():
                command = self.queue.get()
                # Volume changing command. Overrides current fades
                if command.name == Command.volume:
                    self.volume = command.value
                    self.fade_start = None
                # Fade out command. Fade out occurs constantly until silent or overridden by volume change
                elif command.name == Command.fade_out:
                    # Fade out command has no effect if fadeout already underway
                    if self.fade_start is None:
                        # When did the fadeout start?
                        self.fade_start = datetime.now()
            # True if a fade out is in progress
            if self.fade_start is not None:
                # How long has the fade been occurring?
                seconds = (datetime.now() - self.fade_start).total_seconds()
                self.volume *= (1 - self.fade_rate * seconds)
                if self.volume < 0:
                    self.volume = 0
                    self.fade_start = None
            msg.velocity = int(self.volume * msg.velocity)
            # Actually send the midi message
            self.port.send(msg)
            # Check if it was a note message
            if msg.type == Channel.note_on:
                # Keep track of notes that are currently playing
                self.playing_notes.append(msg.note)
                self.playing_notes.sort()
                if self.note_on_listener is not None:
                    self.note_on_listener(msg)
            elif msg.type == Channel.note_off:
                self.playing_notes.remove(msg.note)
        except AttributeError:
            pass

    # Stop all currently playing notes
    def stop_playing_notes(self):
        for note in self.playing_notes:
            self.stop_note(note)

    # Stop a specific note
    def stop_note(self, note):
        # noinspection PyTypeChecker
        self.send_message(mido.Message(type=Channel.note_off, velocity=0, note=note))

    # Set the volume of this channel (float from 0 - 1)
    def set_volume(self, volume):
        self.queue.put(Command(Command.volume, volume))

    # Start fading out this channel
    def fade_out(self):
        self.queue.put(Command(Command.fade_out))

# A bass channel that plays a note from a chord shifted down two octaves on a drum beat
class BassChannel(Channel, object):
    def __init__(self, number, chord_channel, drum_channel, volume=1.0, octave_shift=2):
        super(BassChannel, self).__init__(number, volume)
        self.chord_channel = chord_channel
        self.drum_channel = drum_channel
        self.drum_channel.note_on_listener = self.on_drum_played
        self.pressed_positions = []
        self.octave_shift = octave_shift
        self.pressed_positions_queue = Queue()

    # Called when a drum beat is played
    def on_drum_played(self, msg):
        # Stop all playing notes
        self.stop_playing_notes()
        # Updated pressed positions if waiting in queue
        if not self.pressed_positions_queue.empty():
            self.pressed_positions = self.pressed_positions_queue.get()
        for position in self.pressed_positions:
            try:
                note = self.chord_channel.playing_notes[position % len(self.chord_channel.playing_notes)]
                shift = position / len(self.chord_channel.playing_notes)

                msg.channel = self.number
                msg.note = note + 12 * (shift - self.octave_shift)

                self.send_message(msg)
            except ZeroDivisionError as e:
                logging.exception(e)

    # Set the positions of pressed notes
    def set_pressed_positions(self, pressed_positions):
        self.pressed_positions_queue.put(pressed_positions)

# Represents a midi song loaded from a file
class Song:
    def __init__(self, filename="channels_test.mid", is_looping=False):
        self.filename = filename
        self.queue = Queue()
        self.is_stopping = False
        self.is_looping = is_looping
        self.mid = mido.MidiFile("media/{}".format(self.filename))
        signal.signal(signal.SIGINT, self.stop)
        channel_numbers = range(0, 16)
        self.channels = map(Channel, sorted(list(channel_numbers)))

    # Play the midi file (should be called on new thread)
    def play_midi_file(self):
        self.is_stopping = False

        # Take midi messages from a generator
        for msg in self.mid.play():
            # Break if should stop
            if self.is_stopping:
                break
            # Check if any commands in queue
            if not self.queue.empty():
                command = self.queue.get()
                if isinstance(command, Command):
                    if command.name == Command.pitch_bend:
                        instrument.pitch_bend(command.value)
            try:
                # Send a message to its assigned channel
                self.channels[msg.channel].send_message(msg)
            except AttributeError as e:
                logging.exception(e)
            except IndexError as e:
                logging.exception(e)
        # Loop again if set to do so
        if self.is_looping and not self.is_stopping:
            self.play_midi_file()

    # Start this song on a new thread
    def start(self):
        t = Thread(target=self.play_midi_file)
        t.start()

    # Stop this song
    def stop(self):
        self.is_stopping = True

    # Queue up a command
    def send_command(self, name, value):
        self.queue.put(Command(name, value))

    # Set which channels should be playing
    def set_included_channels(self, pressed_positions):
        for channel in self.channels:
            if channel.number in pressed_positions:
                channel.set_volume(1.0)
            else:
                channel.fade_out()

    # Play all channels
    def include_all_channels(self):
        for channel in self.channels:
            channel.set_volume(1.0)
<commit_msg>obtaining absolute path<commit_after>import mido
from Queue import Queue
import music
from threading import Thread
import logging
import signal
from datetime import datetime
import os

path = os.path.realpath(__file__)
dir_path = os.path.dirname(os.path.realpath(__file__))

print dir_path

# noinspection PyUnresolvedReferences
input_names = mido.get_output_names()

REFACE = 'reface DX'
MPX = 'MPX16'
SIMPLE_SYNTH = 'SimpleSynth virtual input'

CHANNEL_PARTITION = 8

simple_port = None

# noinspection PyBroadException
try:
    instrument = music.MidiInstrument()
except Exception:
    logging.warn("Midi instrument could not be opened")

# Creates a port object corresponding to an instrument if it exists, else to a Simple inbuilt synth
def make_port(name):
    for input_name in input_names:
        if name in input_name:
            name = input_name
    global simple_port
    try:
        # noinspection PyUnresolvedReferences
        return mido.open_output(name)
    except IOError:
        logging.warn("{} not found.".format(name))
        if simple_port is None:
            # noinspection PyUnresolvedReferences
            simple_port = mido.open_output()
        return simple_port

keys_port = make_port(REFACE)
drum_port = make_port(MPX)

# Class representing key-value pairs that can be queued as commands
class Command:
    def __init__(self, name, value=None):
        self.name = name
        self.value = value

    pitch_bend = "pitch_bend"
    volume = "volume"
    fade_out = "fade_out"

# Class representing individual midi channel
class Channel:
    note_off = "note_off"
    note_on = "note_on"

    def __init__(self, number, volume=1.0, fade_rate=0.1, note_on_listener=None):
        self.note_on_listener = note_on_listener
        self.number = number
        self.port = keys_port if number < CHANNEL_PARTITION else drum_port
        self.volume = volume
        self.fade_rate = fade_rate
        self.queue = Queue()
        self.fade_start = None
        self.playing_notes = []

    # Send a midi message to this channel
    def send_message(self, msg):
        try:
            # Are there any commands waiting?
            if not self.queue.empty():
                command = self.queue.get()
                # Volume changing command. Overrides current fades
                if command.name == Command.volume:
                    self.volume = command.value
                    self.fade_start = None
                # Fade out command. Fade out occurs constantly until silent or overridden by volume change
                elif command.name == Command.fade_out:
                    # Fade out command has no effect if fadeout already underway
                    if self.fade_start is None:
                        # When did the fadeout start?
                        self.fade_start = datetime.now()
            # True if a fade out is in progress
            if self.fade_start is not None:
                # How long has the fade been occurring?
                seconds = (datetime.now() - self.fade_start).total_seconds()
                self.volume *= (1 - self.fade_rate * seconds)
                if self.volume < 0:
                    self.volume = 0
                    self.fade_start = None
            msg.velocity = int(self.volume * msg.velocity)
            # Actually send the midi message
            self.port.send(msg)
            # Check if it was a note message
            if msg.type == Channel.note_on:
                # Keep track of notes that are currently playing
                self.playing_notes.append(msg.note)
                self.playing_notes.sort()
                if self.note_on_listener is not None:
                    self.note_on_listener(msg)
            elif msg.type == Channel.note_off:
                self.playing_notes.remove(msg.note)
        except AttributeError:
            pass

    # Stop all currently playing notes
    def stop_playing_notes(self):
        for note in self.playing_notes:
            self.stop_note(note)

    # Stop a specific note
    def stop_note(self, note):
        # noinspection PyTypeChecker
        self.send_message(mido.Message(type=Channel.note_off, velocity=0, note=note))

    # Set the volume of this channel (float from 0 - 1)
    def set_volume(self, volume):
        self.queue.put(Command(Command.volume, volume))

    # Start fading out this channel
    def fade_out(self):
        self.queue.put(Command(Command.fade_out))

# A bass channel that plays a note from a chord shifted down two octaves on a drum beat
class BassChannel(Channel, object):
    def __init__(self, number, chord_channel, drum_channel, volume=1.0, octave_shift=2):
        super(BassChannel, self).__init__(number, volume)
        self.chord_channel = chord_channel
        self.drum_channel = drum_channel
        self.drum_channel.note_on_listener = self.on_drum_played
        self.pressed_positions = []
        self.octave_shift = octave_shift
        self.pressed_positions_queue = Queue()

    # Called when a drum beat is played
    def on_drum_played(self, msg):
        # Stop all playing notes
        self.stop_playing_notes()
        # Updated pressed positions if waiting in queue
        if not self.pressed_positions_queue.empty():
            self.pressed_positions = self.pressed_positions_queue.get()
        for position in self.pressed_positions:
            try:
                note = self.chord_channel.playing_notes[position % len(self.chord_channel.playing_notes)]
                shift = position / len(self.chord_channel.playing_notes)

                msg.channel = self.number
                msg.note = note + 12 * (shift - self.octave_shift)

                self.send_message(msg)
            except ZeroDivisionError as e:
                logging.exception(e)

    # Set the positions of pressed notes
    def set_pressed_positions(self, pressed_positions):
        self.pressed_positions_queue.put(pressed_positions)

# Represents a midi song loaded from a file
class Song:
    def __init__(self, filename="channels_test.mid", is_looping=False):
        self.filename = filename
        self.queue = Queue()
        self.is_stopping = False
        self.is_looping = is_looping
        self.mid = mido.MidiFile("{}/media/{}".format(dir_path, self.filename))
        signal.signal(signal.SIGINT, self.stop)
        channel_numbers = range(0, 16)
        self.channels = map(Channel, sorted(list(channel_numbers)))

    # Play the midi file (should be called on new thread)
    def play_midi_file(self):
        self.is_stopping = False

        # Take midi messages from a generator
        for msg in self.mid.play():
            # Break if should stop
            if self.is_stopping:
                break
            # Check if any commands in queue
            if not self.queue.empty():
                command = self.queue.get()
                if isinstance(command, Command):
                    if command.name == Command.pitch_bend:
                        instrument.pitch_bend(command.value)
            try:
                # Send a message to its assigned channel
                self.channels[msg.channel].send_message(msg)
            except AttributeError as e:
                logging.exception(e)
            except IndexError as e:
                logging.exception(e)
        # Loop again if set to do so
        if self.is_looping and not self.is_stopping:
            self.play_midi_file()

    # Start this song on a new thread
    def start(self):
        t = Thread(target=self.play_midi_file)
        t.start()

    # Stop this song
    def stop(self):
        self.is_stopping = True

    # Queue up a command
    def send_command(self, name, value):
        self.queue.put(Command(name, value))

    # Set which channels should be playing
    def set_included_channels(self, pressed_positions):
        for channel in self.channels:
            if channel.number in pressed_positions:
                channel.set_volume(1.0)
            else:
                channel.fade_out()

    # Play all channels
    def include_all_channels(self):
        for channel in self.channels:
            channel.set_volume(1.0)
<|endoftext|>


<commit_before>story = None

def loadStory(storyIdString):
	global story
	story = __import__("stories." + storyIdString).story

'''
Base class which treats all named arguments as attributes and
all positional arguments as dicts containing named attributes
'''
class Item(object):
	required=[]
	defaults={}

	# populate attributes from positional and
	def __init__(self, *args, **kwargs):
		for data in args[1:]: # exclude self
			for key in data:
				setattr(self, key, data[key])
		for key in kwargs:
			# check that uids are typed as Uid
			if key.lower().endswith("uid"):
				uidType = type(kwargs[key])
				if not(uidType==Uid):
					raise AssertionError("Attribute " + key + " with name ending 'uid' is actually of type " + uidType.__name__)
			setattr(self, key, kwargs[key])

		# raise error if any 'required' attributes are missing
		missing = list()
		for name in type(self).required:
			if not(hasattr(self, name)):
				missing.append(name)
		if len(missing) > 0:
			raise AssertionError(type(self).__name__ + " missing required attributes " + str(missing) )

		for key in type(self).defaults:
			if not hasattr(self, key):
				setattr(self, key, type(self).defaults[key])

class Container(Item):

	def _get_table(self, cls):
		if not(hasattr(self, 'registry')):
			self.registry = dict()
		if not(cls.__name__ in self.registry):
			 self.registry[cls.__name__]=dict()
		return self.registry[cls.__name__]

	'''register any subclass of item to be looked up later by its id'''
	def _register(self, cls, item):
		table = self._get_table(cls)
		if not(item.uid.idString in table):
			table[item.uid.idString]=item
		else:
			raise AssertionError("Lookup table for " + cls.__name__ + " already contains item with uid" + item.uid.idString)

	''''''
	def _lookup(self, cls, uid):
		if not(type(uid) == Uid):
			raise AssertionError("Attempted lookup with type" + type(uid) + "; should be Uid")
		else:
			if hasattr(self, "registry"):
				if cls.__name__ in self.registry:
					table = self.registry[cls.__name__]
					if uid.idString in table:
						return table[uid.idString]
			import pdb; pdb.set_trace()

			raise AssertionError("'" + uid.idString + "' not in " + cls.__name__ + " lookup table")

class Uid:
	def __init__(self, idString):
		self.idString = idString

	"""Override the default Equals behavior"""
	def __eq__(self, other):
		if isinstance(other, self.__class__):
			return self.idString == other.idString
		return False

	def __neq__(self, other):
		return not self.__eq__(other)

	def __hash__(self):
		return hash(type(self).__name__) ^ hash(self.idString)

'''
	A common superclass for items with ids. Accepts a string id argument
	and silently replaces it with a typed Uid object which is required
	by other Uid oriented references, this supports implicit validation
	of data structures (e.g. encouraging you to refer to myPassage.uid in
	preference to the possibly "theuid")
'''
class UidItem(Item):
	required = Item.required + ["uid"]
	def __init__(self, *a, **k):
		# intercept string uids, turn into Uid objects
		uid = k["uid"]
		if type(uid) == str:
			k["uid"]=Uid(uid)
		super().__init__(*a,**k)

class Story(Container):
	required = Container.required + ["startPassageUid"]

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)

	def registerPassage(self, passage):
		return self._register(Passage, passage)

	def lookupPassage(self, passageUid):
		return self._lookup(Passage, passageUid)

	def registerBox(self, box):
		return self._register(Box, box)

	def lookupBox(self, boxUid):
		return self._lookup(Box, boxUid)

class Card(UidItem):
	required = UidItem.required + ["storyUid", "passageUid", "sack"]

class Box(UidItem):
	required = UidItem.required + ["label"]
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.story.registerBox(self)

class Passage(UidItem):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.story.registerPassage(self)

	def handle_tap(self, engine, card):
		raise AssertionError("Not yet implemented")

class PagePassage(Passage):
	required = Passage.required + ["rightBoxUid", "rightText", "nextPassageUid"]
	defaults = dict(Passage.defaults, **{
		"wrongText":None
	})

	def __init__(self, *a, **k):
		super().__init__(*a,**k)

	def handle_tap(self, engine, card):
		box = engine.box
		import pdb; pdb.set_trace()
		if box.uid == self.rightBoxUid:
			nextPassage = self.story.lookupPassage(self.nextPassageUid)
			card.passageUid = nextPassage.uid
			if self.rightBoxUid == nextPassage.rightBoxUid:
				engine.render(self.rightText + "\n...tap to continue")
			else:
				engine.render(self.rightText + "\n...now go to " + self.story.lookupBox(nextPassage.rightBoxUid).label)
		else:
			if self.wrongText == None:
				wrongText = "Please go to " + self.story.lookupBox(self.rightBoxUid).label + " to continue your adventure"
			else:
				wrongText = self.wrongText
			engine.render(wrongText)

'''
A passage which displays its text on tap, then requires you to tap
again to confirm navigation within a certain time. Failing to tap would
ConfirmationPassage(Passage):
	def handle_tap():
'''

'''
# superceded by 'script' attribute which is evaluated in the context of a tap (e.g. "sack.money = sack.money + 5")
# See also https://twine2.neocities.org/1.html for Twine reference features around variables and execution
class ExecutePassage(PagePassage):
	pass
'''

'''
class WaitPassage(PagePassage):
	pass
'''

class ChoicePassage(PagePassage):
	pass

class ConditionalPassage(PagePassage):
	pass

# Condition render
# and text rendering from sack
# Condition routing
# Test Verification of story
# Rendering as graph

class ConsoleBox(Box):
	def render(self, text):
		print(text)
<commit_msg>Fixed implicit bug with Container not being a UidItem. Made story a context manager. Made story a blank card factory with createBlankCard. Added Passage templating. Normalised event method routing signature with (self, engine) throughout. Separated Page progression from rendering, to support Confirmation passage.<commit_after>from util import ticks_ms

story = None

def loadStory(storyIdString):
	storyModulePath = "stories." + storyIdString
	print("Loading story from " + storyModulePath)
	loaded = __import__(storyModulePath)
	module = getattr(loaded, storyIdString)
	return module.story

def getStoryContext():
	global story
	assert story != None, "Code needs to execute in a `with story:` block"
	return story

class Uid:
	def __init__(self, idString):
		self.idString = idString

	"""Override the default Equals behavior"""
	def __eq__(self, other):
		if isinstance(other, self.__class__):
			return self.idString == other.idString
		return False

	def __neq__(self, other):
		return not self.__eq__(other)

	def __hash__(self):
		return hash(type(self).__name__) ^ hash(self.idString)

# valid types which can be passed as the 'uid' value when creating an item
uidInitTypes = [type(None), Uid, str]

'''
Base class which treats all named arguments as attributes and
all positional arguments as dicts containing named attributes
'''
class Item(object):
	required=[]
	defaults={}

	# populate attributes from positional dicts and keyword args
	def __init__(self, *args, **kwargs):
		for data in args[1:]: # exclude self
			for key in data:
				setattr(self, key, data[key])
		for key in kwargs:
			suffix = "uid"
			# check that uids are typed as Uid
			if key.lower().endswith(suffix):
				uidType = type(kwargs[key])
				assert uidType == Uid or uidType == type(None), "Attribute '{0}' with name ending '1' is actually of type {2}".format(key, suffix, str(uidType))
			setattr(self, key, kwargs[key])

		# raise error if any 'required' attributes are missing
		missing = list()
		for name in type(self).required:
			if not(hasattr(self, name)):
				missing.append(name)
		if len(missing) > 0:
			raise AssertionError(type(self).__name__ + " missing required attributes " + str(missing) )

		for key in type(self).defaults:
			if not hasattr(self, key):
				setattr(self, key, type(self).defaults[key])

'''
	A common superclass for items with ids. Accepts a string id argument
	and silently replaces it with a typed Uid object which is required
	by other Uid oriented references, this supports implicit validation
	of data structures (e.g. encouraging you to refer to myPassage.uid in
	preference to the possibly "theuid")
'''
class UidItem(Item):
	required = Item.required + ["uid"]
	def __init__(self, *a, **k):
		# intercept string uids, turn into Uid objects
		if "uid" in k:
			uid = k["uid"]
			uidType = type(uid)
			assert uidType in uidInitTypes, "'uid' is of '{}'. Uids must be one of ()".format(str(uidType),str(uidInitTypes))
			if type(uid) == str: # cast strings to Uid
				k["uid"]=Uid(uid)
		super().__init__(*a,**k)

class Container(UidItem):

	def _get_table(self, cls):
		if not(hasattr(self, 'registry')):
			self.registry = dict()
		if not(cls.__name__ in self.registry):
			 self.registry[cls.__name__]=dict()
		return self.registry[cls.__name__]

	'''register any subclass of item to be looked up later by its id'''
	def _register(self, cls, item):
		table = self._get_table(cls)
		if not(item.uid.idString in table):
			table[item.uid.idString]=item
		else:
			raise AssertionError("Lookup table for " + cls.__name__ + " already contains item with uid" + item.uid.idString)

	''''''
	def _lookup(self, cls, uid):
		if not(type(uid) == Uid):
			raise AssertionError("Attempted lookup with type" + type(uid) + "; should be Uid")
		else:
			if hasattr(self, "registry"):
				if cls.__name__ in self.registry:
					table = self.registry[cls.__name__]
					if uid.idString in table:
						return table[uid.idString]
			raise AssertionError("'" + uid.idString + "' not in " + cls.__name__ + " lookup table")

class Story(Container):
	required = Container.required + ["startPassageUid"]

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)

	def __enter__(self):
		global story
		story = self

	def __exit__(self, type, value, traceback):
		global story
		story = None

	def registerPassage(self, passage):
		return self._register(Passage, passage)

	def lookupPassage(self, passageUid):
		return self._lookup(Passage, passageUid)

	def registerBox(self, box):
		return self._register(Box, box)

	def lookupBox(self, boxUid):
		return self._lookup(Box, boxUid)

	def createBlankCard(self, cardUid):
		return Card(
			uid=cardUid,
			storyUid = self.uid,
			passageUid = self.startPassageUid,
			sack = dict(
				money=100
			)
		)

class Card(UidItem):
	required = UidItem.required + ["storyUid", "passageUid", "sack"]

class Box(UidItem):
	required = UidItem.required + ["label"]
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		getStoryContext().registerBox(self)

class Passage(UidItem):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		global story
		story.registerPassage(self)

	def getRenderedDict(self, engine):
		d = dict()

		# populate using all primitive attributes of Passage
		for key,value in self.__dict__.items():
			valType = type(value)
			if valType==str or valType==int or valType==float or valType==bool:
				d[key]=str(value)

		# populate using all attributes of sack
		for key,value in engine.card.sack.items():
			assert key not in d, "Error processing sack value. RenderedDict already contains '" + key + "'"
			d[key]=value

		return d

	def getRenderedText(self, engine):
		d = self.getRenderedDict(engine)
		nextT = self.getRenderedTemplate(engine)
		prevT = None
		try:
			while nextT != prevT: # repeats until formatted text is the same as unformatted
				prevT = nextT
				nextT = prevT.format(**d)
			return nextT
		except KeyError as k:
			import pdb; pdb.set_trace()
			raise k

	def getRenderedTemplate(self):
		raise AssertionError("Not yet implemented")

	def handleTap(self, engine, card):
		raise AssertionError("Not yet implemented")

class BoxPassage(Passage):
	required = Passage.required + ["rightBoxUid", "rightBoxText"]
	defaults = dict(Passage.defaults,
		wrongBoxText="Please go to {rightBoxLabel} to continue your adventure"
	)

	def getRenderedDict(self, engine):
		d = super().getRenderedDict(engine)
		d.update(
			rightBoxLabel = engine.story.lookupBox(self.rightBoxUid).label
		)
		return d

	def getRenderedTemplate(self, engine):
		box = engine.box
		if box.uid == self.rightBoxUid:
			return "{rightBoxText}"
		else:
			return "{wrongBoxText}"

class PagePassage(BoxPassage):
	required = BoxPassage.required + ["nextPassageUid"]

	def getNextPassage(self, engine):
		return engine.story.lookupPassage(self.nextPassageUid)

	def getNextBox(self, engine):
		nextPassage = self.getNextPassage(engine)
		return engine.story.lookupBox(nextPassage.rightBoxUid)

	def getRenderedDict(self, engine):
		d = super().getRenderedDict(engine)
		d.update(
			nextBoxLabel=self.getNextBox(engine).label
		)
		return d

	def getRenderedTemplate(self, engine):
		template = super().getRenderedTemplate(engine)
		nextPassage = self.getNextPassage(engine)
		if self.rightBoxUid == nextPassage.rightBoxUid:
			return template + "\n...tap to continue"
		else:
			return template + "\n...now go to {nextBoxLabel}"

	def progress_player(self, engine):
		engine.card.passageUid = self.nextPassageUid

	def handleTap(self, engine):
		self.progress_player(engine)
		engine.renderText(self.getRenderedText(engine))

class ConfirmationPassage(PagePassage):
	defaults = dict(PagePassage.defaults,
		tapTime=4000
	)

	def handleTap(self, engine):
		now = ticks_ms()
		# progress only if tap follows quickly
		if now - self.last_tap_ms < self.tapTime:
			self.progress_player(engine.card)
		engine.renderText(self.getRenderedText(engine))
		self.last_tap_ms = now

'''
A passage which displays its text on tap, then requires you to tap
again to confirm navigation within a certain time. Failing to tap would
ConfirmationPassage(Passage):
	def handleTap():
'''

'''
# superceded by 'script' attribute which is evaluated in the context of a tap (e.g. "sack.money = sack.money + 5")
# See also https://twine2.neocities.org/1.html for Twine reference features around variables and execution
class ExecutePassage(PagePassage):
	pass
'''

'''
class WaitPassage(PagePassage):
	pass
'''

class ChoicePassage(PagePassage):
	pass

class ConditionalPassage(PagePassage):
	pass

# Condition render
# and text rendering from sack
# Condition routing
# Test Verification of story
# Rendering as graph
<|endoftext|>