Source code for Level

"""Contains the Level object"""

from typing import List, Union, Dict

import os
import numpy as np

from GramophoneTools.LinMaze import LinMaze, Frame, Event, Rule
from GramophoneTools.LinMaze.Zone import Zone
from GramophoneTools.LinMaze.Tools import Stopwatch, progressbar
from GramophoneTools.LinMaze.Tools.filehandler import select_file


class _Level(object):
    """An object that can be played for conditioning.
    You can add frames, events and rules to it to tune your conditioning. 

    :param name: The name this Level will be refered to by (eg. in the default filename when saving)
    :type name:  str

    :param screen_res: The resolution of the simulation window. Set it to the resolution of the monitor 
        if you are going to make the simulation full screen. eg.: (800,600)
    :type screen_res: int, int

    :param zone_offset: Zone offset in pixels, ie. what is the position of the mouse on the screen from the left.
    :type zone_offset: int

    :param transition_width: How long should the transition (gradual fade) shold be between zones in pixels.
    :type transition_width: int

    :param rgb: Red, Green and Blue levels of the frames as floats between 0 and 1.
    :type rgb: float, float, float
    """

    def __init__(self, name, screen_res, zone_offset,
                 transition_width=100, rgb=(1, 1, 1)):
        print("\nCreating new VR level: ", name, "\n")
        self.name = name
        # self.screen_res = screen_res
        self.screen_width = screen_res[0]
        self.screen_height = screen_res[1]
        self.zone_offset = zone_offset
        self.transition_width = transition_width
        self.rgb = rgb
        self.offset = 0

        self.rendered = False

        self.frames: List[Frame] = []
        self.events: Dict[Event] = {}
        self.zones: List[Zone] = []
        self.rules: List[Rule] = []

    def __str__(self):
        level_string = self.name + '\nResolution: ' + str(self.screen_width)+'×'+str(self.screen_height) + \
            '\nZone offset: ' + str(self.zone_offset) +\
            '\nTransition width: ' + str(self.transition_width)+'\n'

        level_string += "\nBLOCKS\n"
        for frame, zone in zip(self.frames, self.zones):
            level_string += str(frame) + '\n' + str(zone) + '\n\n'

        level_string += "\nRULES\n"
        for rule in self.rules:
            level_string += 'If '+str(rule).lower() + \
                ' then '+str(rule.event).lower()+'\n'

        return level_string

    @property
    def dummy_frame(self):
        """A frame that looks like the begining of the level."""
        dummy_frame = Frame.Frame(self.screen_width, self.screen_height)
        dummy_frame.texture = self.combined_frame.texture[:, :self.screen_width].astype(
            np.uint8)
        dummy_frame.made = True
        return dummy_frame

    @property
    def combined_frame(self):
        """All the frames combined left to right."""
        if not self.rendered:
            self.render()

        return Frame.combine(self.frames)

    @property
    def image(self):
        """An image of the Level's combined frames."""
        return self.combined_frame.make_img()

    def add_block(self, frame_type, **kwargs):
        """Adds a block of the specified type and parameters to the level."""

        frame_type = frame_type.lower()

        width = kwargs.get('length', 0) + self.transition_width
        height = self.screen_height
        wavelength = kwargs.get('wavelength', None)
        angle = kwargs.get('angle', None)
        random_seed = kwargs.get('random_seed', None)
        side_length = kwargs.get('side_length', None)
        filename = kwargs.get('filename', None)

        frame_maker = {  # Frame type switch
            'sine': lambda: Frame.SineWave(width, height, wavelength, angle),
            'square': lambda: Frame.SquareWave(width, height, wavelength, angle),
            'greynoise': lambda: Frame.GreyNoise(width, height, random_seed),
            'binarynoise': lambda: Frame.BinaryNoise(width, height, random_seed),
            'cloud': lambda: Frame.Cloud(width, height, random_seed),
            'marble': lambda: Frame.Marble(width, height, random_seed),
            'wood': lambda: Frame.Wood(width, height, random_seed),
            'checkerboard': lambda: Frame.Checkerboard(width, height, side_length),
            'image': lambda: Frame.ImageFile(height, filename)
        }.get(frame_type, None)

        if frame_maker is None:
            raise ValueError(
                'Error: \'{}\' is not a valid frame_type'.format(frame_type))
        frame = frame_maker()

        # New zone starts at the end of the last one
        offset = 0
        if self.zones:
            offset = self.zones[-1].offset + self.zones[-1].length
        zone = Zone(offset, frame.width, kwargs.get('zone_type', 'generic'))

        self.frames.append(frame)
        self.zones.append(zone)

        print("\nAdded block to " + self.name + ":")
        print(str(frame))
        print(str(zone))

    def add_event(self, name, event_type, *args):
        """Adds an Event to the level which can be triggered by Rules.

        :param name: The name this Event can be referenced by
        :type name: string

        :param event_type: type of the event can be 'teleport', 'random_teleport', 
            'start_burst', 'stop_burst', 'port_on', 'port_off', 'pause', 'unpause'
        :type event_type: string

        :param args: The arguments for the Event of the given type.
        """

        if event_type == "teleport":
            self.events[name] = Event.Teleport(self, args[0])

        if event_type == "random_teleport":
            self.events[name] = Event.RandomTeleport(self, args[0])

        if event_type == "teleport_to_level":
            self.events[name] = Event.RandomTeleport(args[0], args[1])

        if event_type == "start_burst":
            self.events[name] = Event.StartBurst(
                self, args[0], args[1], args[2])

        if event_type == "stop_burst":
            self.events[name] = Event.StopBurst(self, args[0])

        if event_type == "port_on":
            self.events[name] = Event.PortOn(self, args[0])

        if event_type == "port_off":
            self.events[name] = Event.PortOff(self, args[0])

        if event_type == "pause":
            self.events[name] = Event.Pause(self, args[0])

        if event_type == "unpause":
            self.events[name] = Event.UnPause(self, args[0])

        if event_type == "print":
            self.events[name] = Event.Print(self, args[0])

    def add_rule(self, rule_type, event_name, *args):
        """Adds a rule that triggers events based on animal behaviour.

        :param rule_type: The type of this rule, can be 'zone', 'velocity' or 'speed'.
        :type rule_type: string

        :param event_name: The name of the Event this Rule can trigger.
        :type event_name: string

        :param args: The arguments for the Rule of the given type.
        """
        
        if rule_type == "zone":
            self.rules.append(Rule.ZoneRule(
                self, self.events[event_name], args[0], args[1]))
            # zone_type, delay

        if rule_type == "velocity":
            self.rules.append(Rule.VelocityRule(
                self, self.events[event_name], args[0], args[1], args[2]))
            # vel_rule_type, threshold, delay

        if rule_type == "smooth_velocity":
            self.rules.append(Rule.SmoothVelocityRule(
                self, self.events[event_name], args[0], args[1], args[2], args[3]))
            # bin_size, vel_rule_type, threshold, delay

        if rule_type == "speed":
            self.rules.append(Rule.SpeedRule(
                self, self.events[event_name], args[0], args[1], args[2]))
            # speed_rule_type, threshold, bin_size

        if rule_type == "keypress":
            self.rules.append(Rule.KeyPressRule(
                self, self.events[event_name], args[0]))
            # key

        if rule_type == "input":
            self.rules.append(Rule.InputRule(
                self, self.events[event_name], args[0], args[1]))
            # input_id, trigger_type

    def reset_rules(self):
        for rule in self.rules:
            if hasattr(rule, 'delay_timer'):
                rule.delay_timer.reset()

    def render(self):
        """Renders all the frames of the level, making it ready to be played."""
        if not self.rendered:
            print('\n'+self.name,  '>>\n')
            self.frames = Frame.multi_make(self.frames)
            self.frames = Frame.frame_transitions(
                self.frames, self.transition_width)

            loadtime = Stopwatch.Stopwatch()
            for count, frame in enumerate(self.frames):
                frame.make_texture()
                progressbar.printProgressBar(count, len(self.frames),
                                             prefix=' Loading  ',
                                             suffix='  (' + str(loadtime) + ')')
            progressbar.printProgressBar(len(self.frames), len(self.frames),
                                         prefix=' Loading  ',
                                         suffix='  (' + str(loadtime) + ')')
            print('\n')

            self.rendered = True

    def show_image(self):
        """Displays the level as a picture"""
        self.image.show()

    def save_image(self):
        """Saves the picture of the Level"""
        filename = select_file(defaultextension='.bmp',
                               filetypes=[('VR Level image', '.bmp')],
                               title='Save image of Level',
                               initialdir=os.getcwd(),
                               initialfile=self.name)
        self.image.convert('RGB').save(filename)

    def save_summary(self):
        """Saves a human readable summary of the Level"""
        filename = select_file(defaultextension='.txt',
                               filetypes=[('Text summary', '.txt')],
                               title='Save summary of Level',
                               initialdir=os.getcwd(),
                               initialfile=self.name)

        with open(filename, 'w') as summary_file:
            summary_file.write(str(self))

    def play(self, *args, **kwargs):
        """Plays the Level by making a Session for it.

        :param args: Arguments of the Session created.
        :param kwargs: Keyword arguments of the Session created.
        """
        try:
            LinMaze.Session(self, *args, **kwargs)
        except LinMaze.LinMazeError as err:
            print('\nERROR:', err)
            input()

    @property
    def length(self):
        return sum(zone.length for zone in self.zones)

    def get_zone_by_name(self, zone_name: str) -> Union[Zone, None]:
        try:
            return next(zone for zone in self.zones
                        if zone.zone_type == zone_name)
        except StopIteration:
            return None
    
    # @property
    # def offset(self):
    #     return self.index * self.


class LevelCollection:
    screen_width: int
    screen_height: int
    level_list: List[_Level] = []

    # def __new__(cls):
    #     print("new")
    #     return self
    #     self.screen_width, self.screen_height = screen_res
    #     cls.create_level(name, transition_width, rgb)

    def __init__(self, name, zone_offset, screen_res, *args, **kwargs):
        self.name: str = name
        self.zone_offset: int = zone_offset
        self.screen_width, self.screen_height = screen_res

        self.active_level: Union[_Level, None] = None
        self.frames: List[Frame] = []

        self._extra_args = args
        self._extra_kwargs = kwargs

    def create_level(self, name, transition_width=100, rgb=(1, 1, 1)):
        new_level = _Level(name, (self.screen_width, self.screen_height),
                           self.zone_offset, transition_width, rgb)

        self.level_list.append(new_level)
        return new_level

    def __iter__(self):
        return iter(self.level_list)

    def __len__(self):
        return len(self.level_list)

    def index(self, obj: _Level):
        return self.level_list.index(obj)

    @property
    def rendered(self):
        return all(lvl.rendered for lvl in self.level_list)

    @property
    def dummy_frame(self):
        dummy_frame = Frame.Frame(self.screen_width, self.screen_height)
        dummy_frame.texture = self.combined_frame.texture[:, :self.screen_width].astype(
            np.uint8)
        dummy_frame.made = True
        return dummy_frame

    @property
    def combined_frame(self):
        self.render()
        return Frame.combine(self.frames)

    @property
    def image(self):
        return self.combined_frame.make_img()

    def show_image(self):
        self.image.show()

    def render(self):
        self.frames = []
        for lvl in self.level_list:
            lvl.render()
            self.frames += lvl.frames  # list append

    def reset_rules(self):
        for lvl in self.level_list:
            lvl.reset_rules()

    def get_level_by_name(self, level_name: str) -> Union[_Level, None]:
        try:
            return next(level for level in self.level_list
                        if level.name == level_name)
        except StopIteration:
            return None

    def create_default_level(self):
        if not self.level_list:
            new_level = _Level("default_level",
                               (self.screen_width, self.screen_height),
                               self.zone_offset,
                               *self._extra_args,
                               **self._extra_kwargs)
            self.level_list.append(new_level)
            self.active_level = new_level

    def add_block(self, *args, **kwargs):
        """Mimics Level.add_block for compatibility"""
        self.create_default_level()
        self.active_level.add_block(*args, **kwargs)

    def add_event(self, *args, **kwargs):
        """Mimics Level.add_event for compatibility"""
        self.create_default_level()
        self.active_level.add_event(*args, **kwargs)

    def add_rule(self, *args, **kwargs):
        """Mimics Level.add_event for compatibility"""
        self.create_default_level()
        self.active_level.add_rule(*args, **kwargs)

    def all_zones(self):
        zones = []
        for level in self.level_list:
            zones += level.zones
        return zones

    def play(self, *args, **kwargs):
        if not self.level_list:
            raise RuntimeError("Level sequence is empty")

        param_starting_level = kwargs.pop('starting_level', 'default_level')
        if isinstance(param_starting_level, str):
            starting_level = self.get_level_by_name(param_starting_level)
        else:
            starting_level = param_starting_level

        if starting_level is not None:
            self.active_level = starting_level
        else:
            self.active_level = self.level_list[0]

        try:
            LinMaze.Session(self, *args, **kwargs)
        except LinMaze.LinMazeError as err:
            print('\nERROR:', err)
            input()


# compatibility rename
[docs]class Level(LevelCollection): pass