"""
NSymmetricSystem is an experimental extension of CoordinationGraphSystem where edges may have shared policies.

The NSymmetricSystem aims to reduce the size of CoordinationGraphSystem by exploiting symmetries inside the multiagent
    coordination graph. For example, consider an game of soccer where different agents with implicit roles might
    behave similarly with regard to each other.

    In addition, it aims to adapt to deletion by diffusing learned information about joint policies among many agents.

    It might be possible in the future to also exploit the symmetries of NSymmetricSystem to produce a new
        action selection algorithm.

A N-Symmetric System is defined by a degree of partition, n >= 1.

    This is specified in the configuration as "degree".

The policy-sharing method is structured as such:

    Walk through the coordination graph with depth-first traversal, assigning each agent an integer corresponding to
    the number of depths in the traversal where an agent of that agentclass was encountered.

    Take the modulo of the agent-assigned integers with n, the degree of partition.

    Label edges with the unordered duplet of the partitioned agent labels.

    Edges that share the same agent-class agents and the same duplet label will share policies.
"""
from typing import Dict, List, Tuple, Any

from agentsystem import CoordinationGraphSystem
from agentsystem.util import PolicyGroup
from domain.observation import ObservationDomain
from domain.ActionDomain import ActionDomain
from common.domain_transfer import DomainTransferMessage
from config import Config, checks
from config.config import ConfigItemDesc

class NSymmetricSystem(CoordinationGraphSystem):

    @classmethod
    def get_class_config(cls) -> List[ConfigItemDesc]:
        return CoordinationGraphSystem.get_class_config() + [
            ConfigItemDesc(name='degree',
                           check=checks.positive_integer,
                           info='Degree of sharing (n) in the N-Symmetric System. '
                                '\n\t1: All edges with unique joint profiles share the same policy.'
                                '\n\t2 and above: Such edges share policy based on a graph coloring that assigns '
                                'each edge which would have shared a policy to n separate policies.')
        ]

    def __init__(self,
                 agent_id_class_map: Dict[int, int],
                 agent_class_action_domains: Dict[int, ActionDomain],
                 agent_class_observation_domains: Dict[int, ObservationDomain],
                 auxiliary_data: Dict[str, Any],
                 experiment_config: Config):
        # Store values in agent partition
        self.partition_map: Dict[int, int] = {}

        # Map from agent-class and label duplet to Policy. Model is not shared, as with SharedSystem.
        self.shared_policy_map: Dict[Tuple[Any, Any, int, int]] = {}

        self.degree = experiment_config.find_config_for_instance(self).degree

        super().__init__(agent_id_class_map,
                         agent_class_action_domains,
                         agent_class_observation_domains,
                         auxiliary_data,
                         experiment_config)

    def _make_policy_groups(self, agent_class_map: Dict[int, int], agent_class_action_domains: List[ActionDomain],
                            agent_class_observation_domains: List[ObservationDomain]) -> List[PolicyGroup]:
        # Breadth-first traversal and labeling
        queue = []
        for a0 in self.agent_ids:
            ac_depth = {agent_class: 0 for agent_class in set(agent_class_map.values())}
            # Visit and push seed vertex.
            if a0 not in self.partition_map:
                ac0 = agent_class_map[a0]
                self.partition_map[a0] = ac_depth[ac0] % self.degree
                ac_depth[ac0] += 1
                queue.append(a0)
            # Then visit and push neighbors.
            while queue:
                a1 = queue.pop(0)
                visited_ac = set()
                for neighbor in self.graph.agent_neighbors[a1]:
                    if neighbor not in self.partition_map:
                        ac_neighbor = agent_class_map[neighbor]
                        self.partition_map[neighbor] = ac_depth[ac_neighbor] % self.degree
                        visited_ac.add(ac_neighbor)
                        queue.append(neighbor)
                for agent_class in set(agent_class_map.values()):
                    if agent_class in visited_ac:
                        ac_depth[agent_class] += 1
                    else:
                        ac_depth[agent_class] = 0

        return super(NSymmetricSystem, self)._make_policy_groups(agent_class_map,
                                                                 agent_class_action_domains,
                                                                 agent_class_observation_domains)

    def _make_joint_policy(self, edge_id):
        """
        Make the joint policy object for the pair of agents at the defined edge.
        :param edge_id: Edge we are making a policy for.
        :return:
        """
        # See if it already has been made.
        a1, a2 = self.graph[edge_id]
        ap1, ap2 = self.partition_map[a1], self.partition_map[a2]
        policy_identifier: Tuple[Any, Any, int, int] = self.graph.get_edge_class(edge_id) + tuple(sorted([ap1, ap2]))
        if policy_identifier in self.shared_policy_map:
            policy = self.shared_policy_map[policy_identifier]
        else:
            joint_act_dom, joint_obs_dom = self._make_joint_domains(edge_id)
            policy = self.policy_class(joint_obs_dom, joint_act_dom, self.top_config)
        self.shared_policy_map[policy_identifier] = policy
        return policy

    def transfer_domain(self, transfer_domain_message: DomainTransferMessage):
        # TODO - NSymmetricSystem should be different than CGS. We should be able to handle both agent addition and
        # removal by exploiting the shared policy map. However, this method is different from that of CGS, so
        # we will leave it for the (future) when this system is investigated.
        raise NotImplementedError()


'''
Реализуйте базовый класс Car. У данного класса должны быть следующие атрибуты:
speed, color, name, is_police (булево). А также методы: go, stop, turn(direction),
которые должны сообщать, что машина поехала, остановилась, повернула (куда).
Опишите несколько дочерних классов: TownCar, SportCar, WorkCar, PoliceCar.
Добавьте в базовый класс метод show_speed, который должен показывать текущую
скорость автомобиля. Для классов TownCar и WorkCar переопределите метод show_speed.
При значении скорости свыше 60 (TownCar) и 40 (WorkCar) должно выводиться сообщение
о превышении скорости.
Создайте экземпляры классов, передайте значения атрибутов. Выполните доступ к атрибутам,
выведите результат. Выполните вызов методов и также покажите результат.
'''
import random

class Car:
    def __init__(self, speed, color, name, is_police):
        self.speed = speed
        self.color = color
        self.name = name
        self.is_police = is_police

    def go(self):
        return 'Машина поехала'

    def stop(self):
        return 'Машина остановилась'

    def turn(self):
        turn = random.choice(['направо', 'налево'])
        return f'Машина повернула {turn}'

    def show_speed(self):
        current_speed = random.randint(0,180)
        return f'Текущая скорость {current_speed}'

class TownCar(Car):
    def show_speed(self):
        current_speed = random.randint(0,180)
        print(f'Текущая скорость {current_speed}')
        if current_speed > 60:
            print('Внимание! Превышение скорости')

class SportCar(Car):
    def sport(self):
        return 'Это спорткар!'

class WorkCar(Car):
    def show_speed(self):
        current_speed = random.randint(0,180)
        print(f'Текущая скорость {current_speed}')
        if current_speed > 40:
            print('Внимание! Превышение скорости')

class PoliceCar(Car):
    def police(self):
        if self.is_police == True:
            return 'Внимание! Это полиция'
        else:
            return 'Это похожая на полицию машина'

a = Car(90, 'Red', 'Mazda', False)
print(a.name, a.color)
print(a.go())
print(a.turn())
print(a.show_speed())
print('-----------')
b = PoliceCar(100, 'Blue', 'BMW', True)
print(b.name, b.color)
print(b.go())
print(b.turn())
print(b.show_speed())
print(b.police())
print('-----------')


#!/usr/bin/env python
"""
Class for controling a RN2483 with python's Serial module
"""

import serial

class RN2483(object):
    def __init__(self, port):
        self._ser = serial.Serial(port, 57600, timeout=1)
        self.reset()
        print(self.command('sys get ver'))

    def command(self, cmdIn, check=False):
        self._ser.write(cmdIn+'\r\n')
        result = self._ser.readline().splitlines()[0]
        if check and result != 'ok':
            raise Exception('Cmd "%s" responded with "%s"'%(cmdIn, result))
        return result

    def reset(self):
        self.command('sys reset')

    def configLoRa(self,
        mod = 'lora',
        freq = None,
        pwr = -3, #-3 to 15
        sf = 'sf8', #7-12
        crc = 'on', #checksum
        iqi = 'off', #invert
        cr = '4/8', #4/5, 6, 7, 8
        wdt = 0, #0 is disabled
        sync = 0x12, #hex byte sync word
        bw = 125, #125, 250, 500 kHz bandwidth
    ):

        assert(mod == 'lora')
        assert(pwr >= -3 and pwr <= 15)
        assert(sf in ['sf7', 'sf8', 'sf9', 'sf10', 'sf11', 'sf12'])
        assert(crc in ['on', 'off'])
        assert(iqi in ['on', 'off'])
        assert(cr in ['4/5', '4/6', '4/7', '4/8'])
        assert(bw in [125, 250, 500])

        self.command('radio set mod %s'%mod, check=True)
        if freq is not None:
            self.command('radio set freq %d'%int(freq), check=True)
            assert(int(self.command('radio get freq')) == int(freq))
        self.command('radio set pwr %d'%pwr, check=True)
        self.command('radio set sf %s'%sf, check=True)
        self.command('radio set crc %s'%crc, check=True)
        self.command('radio set iqi %s'%iqi, check=True)
        self.command('radio set cr %s'%cr, check=True)
        self.command('radio set wdt %d'%int(wdt), check=True)
        self.command('radio set sync %s'%hex(sync)[2:], check=True)
        self.command('radio set bw %d'%int(bw), check=True)

    def enableCW(self):
        """
        Enable CW, remeber to reset after to use LoRa again.
        """
        self.command('radio cw on')

    def transmit(self, s):
        self.command('mac pause')
        out = ''.join(['%02x'%ord(c) for c in s])
        return self.command('radio tx %s'%out) == 'ok'

    #TODO receive one...

if __name__ == '__main__':
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option("--port", type="string", dest="port", help="TTY device node", default='/dev/ttyACM0')
    parser.add_option("--freq", type="float", dest="freq", help="Tranceiver frequency in Hz", default=868.1e6)
    parser.add_option("--bw", type="float", dest="bw", help="Tranceiver operational BW in Hz", default=250e3)
    parser.add_option("--pwr", type="int", dest="pwr", help="Transmit power in dB [-3 to 15]", default=-3)
    parser.add_option("--sf", type="int", dest="sf", help="Spread factor [7 to 12]", default=8)
    parser.add_option("--crc", action="store_true", dest="crc", help="Specify to use CRC")
    parser.add_option("--cr", type="string", dest="cr", help="Coding rate", default='4/8')
    parser.add_option("--sync", type="string", dest="sync", help="Sync word", default='0x12')
    parser.add_option("--cw", action="store_true", dest="cw", help="Transmit continuous wave")
    parser.add_option("--tx", type="string", dest="tx", help="Transmit the specified message", default=None)
    parser.add_option("--repeat", action="store_true", dest="repeat", help="Repeat transmission")
    (options, args) = parser.parse_args()

    rn2483 = RN2483(options.port)
    rn2483.configLoRa(
        freq=options.freq,
        bw=int(options.bw/1e3),
        pwr=options.pwr,
        sf='sf%d'%options.sf,
        crc='on' if options.crc else 'off',
        cr=options.cr,
        sync=eval(options.sync))

    if options.cw:
        rn2483.enableCW()
        exit(0)

    if options.tx:
        rn2483.transmit(options.tx) #transmit once
        while options.repeat: rn2483.transmit(options.tx)


import torch
from torch import optim
from torch.nn import Parameter
import torch.nn.functional as F

class Encoder(torch.nn.Module):
	def __init__(self, input_size, hidden_size, num_layers, dropout):
		super(Encoder, self).__init__()

		self.input_size = input_size
		self.hidden_size = hidden_size
		self.num_layers = num_layers

		self.fc1 = torch.nn.Linear(input_size, hidden_size*num_layers*2)
		self.fc2 = torch.nn.Linear(hidden_size*num_layers*2, hidden_size*num_layers)
		self.fc3 = torch.nn.Linear(hidden_size*num_layers, hidden_size*num_layers)

		self.bn1 = torch.nn.BatchNorm1d(hidden_size*num_layers*2)
		self.bn2 = torch.nn.BatchNorm1d(hidden_size*num_layers)

		self.dropout1 = torch.nn.Dropout(dropout)
		self.dropout2 = torch.nn.Dropout(dropout)

	def forward(self, inp):
		out = self.fc1(inp)
		out = F.relu(out)
		out = self.bn1(out)
		out = self.dropout1(out)
		out = self.fc2(out)
		out = F.relu(out)
		out = self.bn2(out)
		out = self.dropout2(out)
		out = self.fc3(out)
		out = out.view(-1, self.num_layers, self.hidden_size)
		out = out.permute(1, 0, 2)
		return out

class Decoder(torch.nn.Module):
	def __init__(self, input_size, hidden_size, output_size, num_layers, dropout):
		super(Decoder, self).__init__()

		self.input_size = input_size
		self.hidden_size = hidden_size
		self.output_size = output_size
		self.num_layers = num_layers

		self.lstm = torch.nn.LSTM(input_size=input_size,
								  hidden_size=hidden_size,
								  num_layers=num_layers,
								  batch_first=True,
								  dropout=dropout)
		self.fc = torch.nn.Linear(hidden_size, output_size)

	def forward(self, inp, h0, c0):
		out, (hn, cn) = self.lstm(inp, (h0, c0))
		batch_size = inp.size(0)
		out = out.contiguous()
		out = out.view(-1, out.size(2))
		out = self.fc(out)
		out = out.view(batch_size, -1, out.size(1))
		out = F.log_softmax(out, dim=2)
		return out, (hn, cn)


#!/usr/bin/env python3
from flask import Flask
import json
import re

app = Flask(__name__)

@app.route('/')
def ret_none():
    return "No data available\n";

@app.route('/json')
def ret_json():
    with open("json_updated.json") as f:
        json_data = f.read()
    return json_data

@app.errorhandler(404)
def get404d(num):

    return "Got that 404, bro"

@app.route('/<variable>')
def ret_coords(variable):

    comma_count = 0
    for i, v in enumerate(variable):
        if ',' == v:
            comma_count += 1

    if not comma_count == 1:
        return "Got that comma problem!"

    if not ',' in variable:
        return "Invalid format"

    print(variable)
    # Read the formatted data and compare it to the GPS coordinates
    with open("json_updated.json") as f:
        json_data = f.read()

    # convert the json string into a dictionary
    json_data = json.loads(json_data)

    # get the latitude and longitude from the URL with ',' as a delimiter
    lat, lon = variable.split(',')

    print(lat)
    print(lon)
    try:
        lat = "{0:.2f}".format(float(lat))
        lon = "{0:.2f}".format(float(lon))
    except Exception as e:
        return "There was an exception: " + str(e)
    j = lat+lon

    new_json = dict()
    # set default values for JSON
    new_json = {
        "ASSAULT": 0, "MURDER": 0, "THEFT": 0, "RAPE": 0, "GTA": 0, "ROBBERY": 0, "OTHER": 0
    }
    new_json = json.dumps(new_json)

# checks if there's a coordinates stored
    if j in json_data:
        new_json = json.dumps(json_data[j])
    return new_json

if __name__ == "__main__":
    app.run("0.0.0.0", debug=False, port=80)


import numpy as np

def make_circle_dataset(n_samples: int = 100,
                        factor: float = 0.1,
                        noise: float = 0.05):
    """Credit: https://github.com/scikit-learn/scikit-learn/blob/80598905e517759b4696c74ecc35c6e2eb508cff/sklearn/datasets/_samples_generator.py#L641
  """
    # Number of samples in both the circles.
    n_samples_out = n_samples // 2
    n_samples_in = n_samples - n_samples_out

    # Generate the data
    linspace_out = np.linspace(0, 2 * np.pi, n_samples_out, endpoint=False)
    linspace_in = np.linspace(0, 2 * np.pi, n_samples_in, endpoint=False)
    outer_circ_x = np.cos(linspace_out)
    outer_circ_y = np.sin(linspace_out)
    inner_circ_x = np.cos(linspace_in) * factor
    inner_circ_y = np.sin(linspace_in) * factor

    X = np.vstack([
        np.append(outer_circ_x, inner_circ_x),
        np.append(outer_circ_y, inner_circ_y)
    ]).T
    y = np.hstack([
        np.zeros(n_samples_out, dtype=np.intp),
        np.ones(n_samples_in, dtype=np.intp)
    ])

    generator = np.random.RandomState(42)
    X += generator.normal(scale=noise, size=X.shape)

    return X, np.expand_dims(y, axis=-1)


"""
Zombie Dice is by Steve Jackson Games
http://zombiedice.sjgames.com/

Zombie Dice simulator by Al Sweigart (al@inventwithpython.com)
(I'm not affiliated with SJ Games. This is a hobby project.)

Note: A "turn" is a single player's turn. A "round" is every player having one turn.
Note: Since all variables are public in Python, it is trivial to have a bot that hacks the tournament code. Inspect the bot code before running it.

Instructions for making your own bot can be found here: http://inventwithpython.com/blog/2012/11/21/programming-ai-bots-for-zombie-dice/
"""

__version__ = '0.1.6'

EXCEPTIONS_LOSE_GAME = False # if True, errors in bot code won't stop the tournament code but instead result in the bot losing that game. Leave on False for debugging.
MAX_TURN_TIME = None # number of seconds bot can take per turn. Violating this results in the bot losing the game.

# TODO - I wish there was a way to pre-emptively cut off the bot's turn() call
# after MAX_TURN_TIME seconds have elapsed, but it seems like there's no
# simple way to share GAME_STATE state between the threads/processes.

import logging, random, sys, copy, platform, time, threading, webbrowser, os, re
from collections import namedtuple

# Import correct web server module
here = os.path.abspath(os.path.dirname(__file__))
os.chdir(here)

if platform.python_version().startswith('2.'):
    from SimpleHTTPServer import * # python 2 code
    from SocketServer import *
else:
    from http.server import HTTPServer, SimpleHTTPRequestHandler # python 3 code

logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of the Zombie Dice program.')

# constants, to keep a typo in a string from making weird errors
COLOR = 0
ICON = 1
RED = 'red'
GREEN = 'green'
YELLOW = 'yellow'
SHOTGUN = 'shotgun'
BRAINS = 'brains'
FOOTSTEPS = 'footsteps'

DieRoll = namedtuple('DieRoll', 'color icon')

TOURNAMENT_STATE = None

GAME_STATE = { # Just so that this dict is shareable for user's bot code, don't reassign this variable ever. (Poor design, but it's made for implementation simplicity for newbie programmers.)
    'CURRENT_ZOMBIE': None, # string of the zombie whose turn it is currently
    'CURRENT_CUP': None, # list of dice strings (i.e. 'red', 'yellow', 'green')
    'CURRENT_HAND': None, # list of dice being rolled (should always be length of three)
    'SHOTGUNS_ROLLED': None, # number of shotguns rolled this turn
    'BRAINS_ROLLED': None, # number of brains rolled this turn
    'ROLLED_BRAINS_DETAILS': None, # list of dice strings for each brain rolled, used in the rare event we run out of brain dice
    'TURN_START_TIME': None, # time since unix epoch that the current turn began
}

# Web GUI stuff:
WEB_SERVER_PORT = random.randint(49152, 61000)
SCORE_BAR_MAX_WIDTH = 350 # width in pixels in the web ui for the score bar
TOURNAMENT_RUNNING = False
WEB_GUI_NUM_GAMES = None
START_TIME = None

def assignUniqueZombieName(zombies):
    # Assign names to zombies missing names:
    for zombie in zombies:
        if not hasattr(zombie, 'name') or zombie.name is None:
            zombie.name = zombie.__class__.__name__
        elif not isinstance(zombie.name, str):
            zombie.name = str(zombie.name)

    # Assign names to zombies with duplicate names:
    for zombie in zombies:
        otherZombiesNames = [z.name for z in zombies]
        otherZombiesNames.remove(zombie.name)

        # Check if a duplicate name exists:
        if zombie.name in otherZombiesNames:
            i = 2
            while zombie.name + str(i) in otherZombiesNames:
                i += 1
            zombie.name = zombie.name + str(i)

def runWebGui(zombies, numGames):
    global BOTS, NUM_GAMES
    assignUniqueZombieName(zombies)
    BOTS = list(zombies)
    NUM_GAMES = numGames
    print('Zombie Dice Visualization is running. Open your browser to http://localhost:%s to view it.' % (WEB_SERVER_PORT))
    print('Press Ctrl-C to quit.')
    broswerOpenerThread = BrowserOpener()
    broswerOpenerThread.start()

    if platform.python_version().startswith('2.'):
        httpd = TCPServer(('localhost', WEB_SERVER_PORT), ZombieDiceHandler) # python 2 code
    else:
        httpd = HTTPServer(('localhost', WEB_SERVER_PORT), ZombieDiceHandler) # python 3 code
    try:
        httpd.serve_forever()
    except (KeyboardInterrupt, SystemExit):
        httpd.socket.close()
        sys.exit('Quitting...')

def runGame(zombies):
    """Runs a single game of zombie dice. zombies is a list of zombie dice bot objects."""
    global GAME_STATE
    assignUniqueZombieName(zombies)

    # create a new game state object
    playerScores = dict([(zombie.name, 0) for zombie in zombies])
    playerOrder = [zombie.name for zombie in zombies]
    logging.debug('Player order: ' + ', '.join(playerOrder))

    GAME_STATE['ORDER'] = playerOrder
    GAME_STATE['SCORES'] = playerScores
    GAME_STATE['ROUND'] = 0

    # validate zombie objects, return None to signify an aborted game
    if len(playerOrder) != len(set(playerOrder)): # set() will get rid of any duplicates
        logging.error('Zombies must have unique names.')
        return
    if len(playerOrder) < 2:
        logging.error('Need at least two zombies to play.')
        return
    for zombie in zombies:
        if 'turn' not in dir(zombie):
            logging.error('All zombies need a turn() method.')
        if 'name' not in dir(zombie):
            logging.error('All zombies need a name member.')

    # call every zombie's newGame() method, if it has one
    for zombie in zombies:
        if 'newGame' in dir(zombie):
            zombie.newGame()

    # set up for a new game
    lastRound = False # True when a player has reached 13 brains
    tieBreakingRound = False # True when the "last round" ended in a tie
    zombiesInPlay = copy.copy(zombies) # all zombies play
    crashedBots = [] # list of bots that had exceptions
    while True: # game loop
        GAME_STATE['ROUND'] += 1
        logging.debug('ROUND #%s, scores: %s' % (GAME_STATE['ROUND'], GAME_STATE['SCORES']))

        for zombie in zombiesInPlay:
            if zombie in crashedBots:
                continue
            GAME_STATE['CURRENT_ZOMBIE'] = zombie.name
            logging.debug("%s's turn." % (GAME_STATE['CURRENT_ZOMBIE']))

            # set up for a new turn
            GAME_STATE['CURRENT_CUP'] = [RED] * 3 + [YELLOW] * 4 + [GREEN] * 6
            random.shuffle(GAME_STATE['CURRENT_CUP'])
            GAME_STATE['CURRENT_HAND'] = []
            GAME_STATE['SHOTGUNS_ROLLED'] = 0
            GAME_STATE['BRAINS_ROLLED'] = 0
            GAME_STATE['ROLLED_BRAINS_DETAILS'] = [] # list of dice colors, in case of "ran out of dice"

            # run the turn
            try:
                stateCopy = copy.deepcopy(GAME_STATE)
                GAME_STATE['TURN_START_TIME'] = time.time()
                zombie.turn(stateCopy) # (don't pass the original GAME_STATE)
            except Exception:
                crashedBots.append(zombie)
                if EXCEPTIONS_LOSE_GAME:
                    # if the bot code has an unhandled exception, it
                    # automatically loses this game
                    GAME_STATE['SCORES'][zombie.name] = -1
                    logging.warn('%s has lost the game due to a raised exception.' % (GAME_STATE['CURRENT_ZOMBIE']))
                else:
                    raise # crash the tournament program (good for debugging)
            if GAME_STATE['SHOTGUNS_ROLLED'] < 3:
                logging.debug('%s stops.' % (GAME_STATE['CURRENT_ZOMBIE']))
            else:
                logging.debug('%s is shotgunned. Lose all brains.' % (GAME_STATE['CURRENT_ZOMBIE']))

            # add brains to the score
            if GAME_STATE['SHOTGUNS_ROLLED'] < 3:
                GAME_STATE['SCORES'][zombie.name] += GAME_STATE['BRAINS_ROLLED']

            if GAME_STATE['SCORES'][zombie.name] >= 13:
                # once a player reaches 13 brains, it becomes the last round
                lastRound = True
                logging.debug('Last round. (%s has reached 13 brains.)' % (zombie.name))

        if tieBreakingRound:
            break # there is only one tie-breaking round, so after it end the game

        if lastRound:
            # only zombies tied with the highest score go on to the tie-breaking round (if there is one)
            zombiesInPlay = []
            highestScore = max(GAME_STATE['SCORES'].values()) # used for tie breaking round
            # zombiesInPlay will now only have the zombies tied with the highest score:
            zombiesInPlay = [zombie for zombie in zombies if GAME_STATE['SCORES'][zombie.name] == highestScore]

            if len(zombiesInPlay) == 1:
                # only one winner, so end the game
                break
            else:
                # multiple winners, so go on to the tie-breaking round.
                logging.debug('Tie breaking round with %s' % (', '.join([zombie.name for zombie in zombiesInPlay])))
                tieBreakingRound = True

    # call every zombie's endGame() method, if it has one
    for zombie in zombies:
        if 'endGame' in dir(zombie):
            zombie.endGame(copy.deepcopy(GAME_STATE))

    # rank bots by score
    ranking = sorted(GAME_STATE['SCORES'].items(), key=lambda x: x[1], reverse=True)
    highestScore = ranking[0][1]
    logging.debug('Final Scores: %s' % (', '.join(['%s %s' % (x[0], x[1]) for x in ranking])))     #(', '.join(['%s %s' % (name, score) for name, score in ranking.items()])))

    # winners are the bot(s) with the highest score
    winners = [x[0] for x in ranking if x[1] == highestScore]
    logging.debug('Winner%s: %s' % ((len(winners) != 1 and 's' or ''), ', '.join(winners)))

    return GAME_STATE

def runTournament(zombies, numGames):
    """A tournament is one or more games of Zombie Dice. The bots are re-used between games, so they can remember previous games.
    zombies is a list of zombie bot objects. numGames is an int of how many games to run."""
    global TOURNAMENT_STATE
    zombies = list(zombies)

    TOURNAMENT_STATE = {'GAME_NUMBER': 0,
                        'WINS': dict([(zombie.name, 0) for zombie in zombies]),
                        'TIES': dict([(zombie.name, 0) for zombie in zombies])}

    print('Tournament of %s games started...' % (numGames))

    for TOURNAMENT_STATE['GAME_NUMBER'] in range(numGames):
        random.shuffle(zombies) # randomize the order of the bots
        endState = runGame(zombies) # use the same zombie objects so they can remember previous games.

        if endState is None:
            sys.exit('Error when running game.')

        # Sort out the scores and find the winner.
        ranking = sorted(endState['SCORES'].items(), key=lambda x: x[1], reverse=True)
        highestScore = ranking[0][1]
        winners = [x[0] for x in ranking if x[1] == highestScore]
        if len(winners) == 1:
            TOURNAMENT_STATE['WINS'][ranking[0][0]] += 1
        elif len(winners) > 1:
            for score in endState['SCORES'].items():
                if score[1] == highestScore:
                    TOURNAMENT_STATE['TIES'][score[0]] += 1

    TOURNAMENT_STATE['GAME_NUMBER'] += 1 # increment to show all games are finished

    # print out the tournament results in neatly-formatted columns.
    print('Tournament results:')
    maxNameLength = max([len(zombie.name) for zombie in zombies])

    winsRanking = sorted(TOURNAMENT_STATE['WINS'].items(), key=lambda x: x[1], reverse=True)
    print('Wins:')
    for winnerName, winnerScore in winsRanking:
        print('    %s %s' % (winnerName.rjust(maxNameLength), str(winnerScore).rjust(len(str(numGames)))))

    tiesRanking = sorted(TOURNAMENT_STATE['TIES'].items(), key=lambda x: x[1], reverse=True)
    print('Ties:')
    for tiedName, tiedScore in tiesRanking:
        print('    %s %s' % (tiedName.rjust(maxNameLength), str(tiedScore).rjust(len(str(numGames)))))

def roll():
    """This global function is called by a zombie bot object to indicate that they wish to roll the dice.
    The state of the game and previous rolls are held in global variables."""
    global GAME_STATE

    if MAX_TURN_TIME is not None and (time.time() - GAME_STATE['TURN_START_TIME']) > MAX_TURN_TIME:
        # if the bot code has taken too long, it
        # automatically loses this game
        #GAME_STATE['SCORES'][zombie.name] = -1
        logging.warn('%s has lost the game due to taking too long.' % (GAME_STATE['CURRENT_ZOMBIE']))
        raise Exception('Exceeded max turn time.')

    # make sure zombie can actually roll
    if GAME_STATE['SHOTGUNS_ROLLED'] >= 3:
        return None

    logging.debug(GAME_STATE['CURRENT_ZOMBIE'] + ' rolls. (brains: %s, shotguns: %s)' % (GAME_STATE['BRAINS_ROLLED'], GAME_STATE['SHOTGUNS_ROLLED']))

    # "ran out of dice", so put the rolled brains back into the cup
    if 3 - len(GAME_STATE['CURRENT_HAND']) > len(GAME_STATE['CURRENT_CUP']):
        logging.debug('Out of dice! Putting rolled brains back into cup.')
        GAME_STATE['CURRENT_CUP'].extend(GAME_STATE['ROLLED_BRAINS_DETAILS'])
        GAME_STATE['ROLLED_BRAINS_DETAILS'] = []

    # add new dice to hand from cup until there are 3 dice in the hand
    while len(GAME_STATE['CURRENT_HAND']) < 3:
        newDie = random.choice(GAME_STATE['CURRENT_CUP'])
        logging.debug('%s die added to hand from cup.' % (newDie))
        GAME_STATE['CURRENT_CUP'].remove(newDie)
        GAME_STATE['CURRENT_HAND'].append(newDie)

    # roll the dice
    logging.debug('Hand is %s' % (', '.join(GAME_STATE['CURRENT_HAND'])))
    logging.debug('Cup has %s: %s' % (len(GAME_STATE['CURRENT_CUP']), ', '.join(GAME_STATE['CURRENT_CUP'])))
    diceRollResults = {SHOTGUN: 0, FOOTSTEPS: 0, BRAINS: 0, 'rolls': []}
    for die in GAME_STATE['CURRENT_HAND']:
        dieRollResult = rollDie(die)
        diceRollResults['rolls'].append(dieRollResult)
        diceRollResults[dieRollResult[ICON]] += 1 # increase the shotgun/brain/footstep count

    logging.debug('%s rolled %s' % (GAME_STATE['CURRENT_ZOMBIE'], diceRollResults))

    # count the shotguns and remove them from the hand
    for dieRollResult in diceRollResults['rolls']:
        if dieRollResult[ICON] == SHOTGUN:
            GAME_STATE['SHOTGUNS_ROLLED'] += 1
            logging.debug('Removing ' + dieRollResult[COLOR] + ' from hand for shotgun.')
            GAME_STATE['CURRENT_HAND'].remove(dieRollResult[COLOR])

    # count the brains and remove them from the hand
    for dieRollResult in diceRollResults['rolls']:
        if dieRollResult[ICON] == BRAINS:
            GAME_STATE['ROLLED_BRAINS_DETAILS'].append(dieRollResult[COLOR])
            GAME_STATE['BRAINS_ROLLED'] += 1
            logging.debug('Removing ' + dieRollResult[COLOR] + ' from hand for brains.')
            GAME_STATE['CURRENT_HAND'].remove(dieRollResult[COLOR])

    return diceRollResults

def rollDie(die):
    """Returns the result of a single die roll as a DieRoll namedtuple.
    Index 0 of the tuple is the color of the die (i.e. 'green', 'yellow', 'red').
    Index 1 is the icon: 'shotgun', 'footsteps', 'brains'."""
    roll = random.randint(1, 6)
    if die == RED:
        if roll in (1, 2, 3):
            return DieRoll(RED, SHOTGUN)
        elif roll in (4, 5):
            return DieRoll(RED, FOOTSTEPS)
        elif roll in (6,):
            return DieRoll(RED, BRAINS)
    elif die == YELLOW:
        if roll in (1, 2):
            return DieRoll(YELLOW, SHOTGUN)
        elif roll in (3, 4):
            return DieRoll(YELLOW, FOOTSTEPS)
        elif roll in (5, 6):
            return DieRoll(YELLOW, BRAINS)
    elif die == GREEN:
        if roll in (1,):
            return DieRoll(GREEN, SHOTGUN)
        elif roll in (2, 3):
            return DieRoll(GREEN, FOOTSTEPS)
        elif roll in (4, 5, 6):
            return DieRoll(GREEN, BRAINS)

# ========================================================================
# Web Gui Code:
# ========================================================================

class ZombieDiceHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        pass # comment out this entire method if you want to see the original HTTP log messages.

    def output(self, msg):
        self.send_header('Content-type','text/html')
        self.end_headers()
        self.wfile.write(msg.encode('ascii'))

    def moreoutput(self, msg):
        self.wfile.write(msg.encode('ascii'))

    def do_GET(self):
        global WEB_GUI_NUM_GAMES, START_TIME, TOURNAMENT_RUNNING
        self.send_response(200)
        reqPath = os.path.join(os.getcwd(), os.path.normpath(self.path[1:]))

        if os.path.isfile(reqPath):
            self.serveFile(reqPath)

        elif self.path == '/mainstatus':
            self.renderStatus()

        elif self.path == '/score':
            self.renderScoreJavascript()

        elif self.path.startswith('/start'):
            # "/start/<NUM GAMES>" is visited when the player clicks the "Begin Tournament" button. Check the path for the number of games to run.
            self.beginTournamentButtonPressed()

        elif self.path == '/':
            self.renderMainPage()

    def serveFile(self, reqPath):
        mimeTypeMapping = {'.js': 'application/x-javascript',
                           '.html': 'text/html',
                           '.css': 'text/css',
                           '.png': 'image/png',
                           '.gif': 'image/gif',
                           '.jpg': 'image/jpeg'}
        ending = reqPath[reqPath.rfind('.'):]
        if ending in mimeTypeMapping:
            self.send_header('Content-type',mimeTypeMapping[ending])
        else:
            self.send_header('Content-type','text/plain')
        self.end_headers()
        fp = open(reqPath, 'rb')
        self.wfile.write(fp.read())

    def renderStatus(self):
        if not TOURNAMENT_RUNNING:
            # display the "Begin Tournament" button.
            self.output("""

                  Run <input type="text" size="4" id="numGamesToRun" value="%s"> simulated games.

                  <input type="button" value="Begin Tournament" onclick="startTournament(); return false;" />

                """ % (NUM_GAMES))
        else:
            # display the current status of the tournament simulation that is in progress
            self.output("""

                %s / %s Games Run
                Estimate Time Remaining: %s
                """ % (TOURNAMENT_STATE['GAME_NUMBER'], WEB_GUI_NUM_GAMES, estTimeRemaining(START_TIME, TOURNAMENT_STATE['GAME_NUMBER'], WEB_GUI_NUM_GAMES)))

            if TOURNAMENT_STATE['GAME_NUMBER'] == WEB_GUI_NUM_GAMES:
                # the javascript code checks for this text to know when to stop making repeated ajax requests for status updates
                self.moreoutput('(Refresh page to run a new tournament.)')

    # Returns JavaScript that will be evaluated by eval() in the web page (elegant solution, I know) to update the score table.
    def renderScoreJavascript(self):
        self.send_header('Content-type','text/html')
        self.end_headers()

        if TOURNAMENT_RUNNING and WEB_GUI_NUM_GAMES is not None and START_TIME is not None and TOURNAMENT_STATE['GAME_NUMBER'] is not None:
            for zombieName in [bot.name for bot in BOTS]:
                predictedMaxWidth = int(SCORE_BAR_MAX_WIDTH * max(int(len(BOTS) / 2.0), 1)) # We'll assume that the bots mostly evenly win games
                #predictedMaxWidth = SCORE_BAR_MAX_WIDTH # If the score bar keeps getting too long, just uncomment this line

                scoreBarLength = int((TOURNAMENT_STATE['WINS'][zombieName] / float(WEB_GUI_NUM_GAMES)) * predictedMaxWidth)
                scoreBarColor = getScoreBarColor(zombieName, TOURNAMENT_STATE['WINS'])
                wins = TOURNAMENT_STATE['WINS'][zombieName]
                ties = TOURNAMENT_STATE['TIES'][zombieName]

                escapedZombieName = zombieName.replace(' ', '_') # JavaScript code can't handle zombie name with spaces. TODO - probably can't handle other characters too.
                self.moreoutput("$('#%s_scorebar').css('width', '%spx'); " % (escapedZombieName, scoreBarLength))
                self.moreoutput("$('#%s_scorebar').css('background-color', '#%s'); " % (escapedZombieName, scoreBarColor))
                self.moreoutput("$('#%s_wins').text('%s'); " % (escapedZombieName, wins))
                self.moreoutput("$('#%s_ties').text('%s'); " % (escapedZombieName, ties))

    def beginTournamentButtonPressed(self):
        global WEB_GUI_NUM_GAMES, TOURNAMENT_RUNNING, START_TIME

        # path will be set to "/start/<NUM GAMES>"
        mo = re.search(r'(\d+)', self.path)
        if mo is not None:
            WEB_GUI_NUM_GAMES = int(mo.group(1))
        else:
            WEB_GUI_NUM_GAMES = 1000 # default to 1000
        START_TIME = time.time()

        # start the tournament simulation in a separate thread
        tournamentThread = TournamentThread()
        tournamentThread.start()
        TOURNAMENT_RUNNING = True # TOURNAMENT_RUNNING remains True after the tournament completes, until the "/" page is reloaded. Then it is set to False.
        self.output('') # return blank http reply so the browser doesn't try the request again.

    def renderMainPage(self):
        global WEB_GUI_NUM_GAMES, TOURNAMENT_RUNNING, START_TIME

        # when this page is loaded, if the previous tournmaent completed then restart the tournament:
        if WEB_GUI_NUM_GAMES is not None and TOURNAMENT_STATE['GAME_NUMBER'] == WEB_GUI_NUM_GAMES:
            TOURNAMENT_RUNNING = False # set to True after user clicks the "Begin Tournament" button in the web ui and the tournamentThread starts running.
            WEB_GUI_NUM_GAMES = None # TODO - make this a member variable instead of a global
            START_TIME = None # timestamp of when the tournament started, used for the "estimated time remaining"
            #TOURNAMENT_STATE['GAME_NUMBER'] = 0 #

        # create the table where each bot has a row for its score
        scoreTableHtml = []
        for zombieName in sorted([bot.name for bot in BOTS]):
            escapedZombieName = zombieName.replace(' ', '_') # JavaScript code can't handle zombie name with spaces. TODO - probably can't handle other characters too.
            scoreTableHtml.append('
%s

' % (zombieName, SCORE_BAR_MAX_WIDTH, escapedZombieName, escapedZombieName, escapedZombieName))
        scoreTableHtml = ''.join(scoreTableHtml)

        # output the main page's html (with the score table)
        self.output("""<!DOCTYPE html>

            <title>Zombie Dice Simulator</title>

By Al Sweigart
https://inventwithpython.com
Buy Zombie Dice Online
Program your own Zombie Dice bot.

	Wins	Ties
            %s

            """ % (scoreTableHtml))

# The bot in the lead has a bright red #FF0000 bar, whereas a bot at 0 wins has a black #000000 bar. Depending on where in between the bot's score is, the appropriate black-to-red color is returned. Returns a string like 'FF0000', without the leading '#' character.
def getScoreBarColor(zombieName, winsState):
    maxScore = max(winsState.values())
    myScore = winsState[zombieName]

    if maxScore == 0:
        return '000000' # return black color to prevent zero division error

    redness = int((myScore / float(maxScore)) * 255)
    redness = hex(redness)[2:].upper()
    if len(redness) == 1:
        redness = '0' + redness
    return redness + '0000' # return the HTML RGB color for the bar

# Calculates amount of time remaining for this tournament, given how long it has taken to run the previous games.
def estTimeRemaining(startTime, currentGame, totalGames):
    lapsed = time.time() - startTime
    if currentGame == 0:
        return 'Unknown' # prevent zero division
    totalEstTime = lapsed * (totalGames / float(currentGame))
    return prettyTime(int(totalEstTime - lapsed))

# Takes parameter that is a number of seconds and returns a pretty string with the time in weeks, days, hours, minutes, and seconds.
def prettyTime(t): # t is in seconds
    wk = day = hr = min = sec = 0
    if t > 604800:
        wk = t // 604800
        t = t % 604800
    if t > 86400:
        day = t // 86400
        t = t % 86400
    if t > 3600:
        hr = t // 3600
        t = t % 3600
    if t > 60:
        min = t // 60
        t = t % 60
    sec = t

    t_str = []
    if wk > 0:
        t_str.append('%s wk' % (wk))
    if wk > 0 or day > 0:
        t_str.append('%s day' % (day))
    if wk > 0 or day > 0 or hr > 0:
        t_str.append('%s hr' % (hr))
    if wk > 0 or day > 0 or hr > 0 or min > 0:
        t_str.append('%s min' % (min))
    t_str.append('%s sec' % (sec))

    return ' '.join(t_str[:2])

# Runs the zombie dice tournament in a separate thread.
class TournamentThread(threading.Thread):
    def run(self):
        runTournament(BOTS, WEB_GUI_NUM_GAMES)

class BrowserOpener(threading.Thread):
    def run(self):
        time.sleep(0.4) # give the server a bit of time to start
        webbrowser.open('http://localhost:%s' % (WEB_SERVER_PORT))

from . import examples

def demo():
    zombies = (
        examples.RandomCoinFlipZombie(name='Random'),
        examples.MonteCarloZombie(name='Monte Carlo', riskiness=40, numExperiments=20),
        examples.RollsUntilInTheLeadZombie(name='Until Leading'),
        examples.MinNumShotgunsThenStopsZombie(name='Stop at 2 Shotguns', minShotguns=2),
        examples.MinNumShotgunsThenStopsZombie(name='Stop at 1 Shotgun', minShotguns=1),
        examples.AlwaysRollsTwiceZombie(name='Roll Twice'),
        # Add any other zombie players here.
    )

    # Uncomment one of the following lines to run in CLI or Web GUI mode:
    #zombiedice.runTournament(zombies=zombies, numGames=100)
    runWebGui(zombies=zombies, numGames=1000)


import time
import functools
from sklearn.ensemble import RandomForestClassifier
from sklearn import svm
from sklearn.model_selection import RandomizedSearchCV
from sklearn.model_selection import StratifiedKFold, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn import metrics
from sklearn.metrics import classification_report, confusion_matrix
import numpy as np

def timer(func):
    '''
    Decorator to log time taken by classifier
    '''
    # from https://realpython.com/primer-on-python-decorators/

    # print runtime of decorated function
    @functools.wraps(func)

    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value

    return wrapper_timer

@timer
def run_linear_svm_classification(X, y, X_test, y_test, scoring_measure, random_seed):

    '''This part of the code was partly inspired by Sandra Vieira, Rafael Garcia-Dias, Walter Hugo Lopez Pinaya - Chapter 19: A Step-By-Step Tutorial On How To Build A Machine Learning Model (https://github.com/MLMH-Lab/How-To-Build-A-Machine-Learning-Model/)'''

    # scaling
    scaler = StandardScaler()

    # X and X_test are images, convert to flat matrices
    X_flat = list(X)
    X_flat = np.nan_to_num(X_flat)
    scaler.fit(X_flat)
    X_flat = np.nan_to_num(scaler.transform(X_flat))

    X_test_flat = list(X_test)
    X_test_flat = np.nan_to_num(X_test_flat)
    X_test_flat = np.nan_to_num(scaler.transform(X_test_flat))

    y = y.astype('category').cat.codes.values
    y_test = y_test.astype('category').cat.codes.values

    clf = svm.LinearSVC(random_state=random_seed, max_iter= 1000000)

    param_grid = {'C': [2 ** -2, 2 ** -1, 2 ** 0, 2 ** 1, 10, 25, 50, 100], 'penalty' : ['l2']}

    # grid search
    internal_cv = StratifiedKFold(n_splits=2)
    grid_cv = GridSearchCV(estimator=clf,
                            param_grid=param_grid,
                            cv=internal_cv,
                            scoring=scoring_measure,
                            verbose=1)

    grid_result = grid_cv.fit(X_flat, y)

    print('Best: %f using %s' % (grid_result.best_score_, grid_result.best_params_))
    means = grid_result.cv_results_['mean_test_score']
    stds = grid_result.cv_results_['std_test_score']
    params = grid_result.cv_results_['params']

    for mean, stdev, param in zip(means, stds, params):
        print('%f (%f) with: %r' % (mean, stdev, param))

    print("Best estimator: ", grid_cv.best_estimator_)

    best_clf = grid_cv.best_estimator_

    prediction = best_clf.predict(X_test_flat)

    print('Confusion matrix')
    cm = confusion_matrix(y_test, prediction)
    print(cm)

    tn, fp, fn, tp = cm.ravel()

    bac_test = metrics.balanced_accuracy_score(y_test, prediction)
    sens_test = tp / (tp + fn)
    spec_test = tn / (tn + fp)

    print('Balanced accuracy: %.3f ' % bac_test)
    print('Sensitivity: %.3f ' % sens_test)
    print('Specificity: %.3f ' % spec_test)

    coefs = best_clf.coef_

    return prediction, bac_test, sens_test, spec_test, cm, coefs

@timer
def run_nonlinear_svm_classification(X, y, X_test, y_test, scoring_measure, random_seed):

    # scaling
    scaler = StandardScaler()

    # X and X_test are images, convert to flat matrices
    X_flat = list(X)
    X_flat = np.nan_to_num(X_flat)
    scaler.fit(X_flat)
    X_flat = np.nan_to_num(scaler.transform(X_flat))

    X_test_flat = list(X_test)
    X_test_flat = np.nan_to_num(X_test_flat)
    X_test_flat = np.nan_to_num(scaler.transform(X_test_flat))

    y = y.astype('category').cat.codes.values
    y_test = y_test.astype('category').cat.codes.values

    clf = svm.SVC(kernel = 'poly', random_state=random_seed, max_iter= 1000000)

    param_grid = {'C': [2 ** -6, 2 ** -5, 2 ** -4, 2 ** -3, 2 ** -2, 2 ** -1, 2 ** 0, 2 ** 1, 10, 25, 50, 100]}

    # grid search
    internal_cv = StratifiedKFold(n_splits=2)
    grid_cv = GridSearchCV(estimator=clf,
                            param_grid=param_grid,
                            cv=internal_cv,
                            scoring=scoring_measure,
                            verbose=1)

    grid_result = grid_cv.fit(X_flat, y)

    print('Best: %f using %s' % (grid_result.best_score_, grid_result.best_params_))
    means = grid_result.cv_results_['mean_test_score']
    stds = grid_result.cv_results_['std_test_score']
    params = grid_result.cv_results_['params']

    for mean, stdev, param in zip(means, stds, params):
        print('%f (%f) with: %r' % (mean, stdev, param))

    print("Best estimator: ", grid_cv.best_estimator_)

    best_clf = grid_cv.best_estimator_

    prediction = best_clf.predict(X_test_flat)

    print('Confusion matrix')
    cm = confusion_matrix(y_test, prediction)
    print(cm)

    tn, fp, fn, tp = cm.ravel()

    bac_test = metrics.balanced_accuracy_score(y_test, prediction)
    sens_test = tp / (tp + fn)
    spec_test = tn / (tn + fp)

    print('Balanced accuracy: %.3f ' % bac_test)
    print('Sensitivity: %.3f ' % sens_test)
    print('Specificity: %.3f ' % spec_test)

    coefs = best_clf.coef_

    return prediction, bac_test, sens_test, spec_test, cm, coefs

@timer
def run_rf_classification(X, y, X_test, y_test, scoring_measure, random_seed):

    ''' This part of the code was partly inspired by Ruben Otter - Unsupervised Gaze Event Discrimination (https://github.com/LEO-UMCG/Unsupervised-Gaze-Event-Discrimination/)'''

   # scaling to variance but keep sparsity structure of data
    scaler = StandardScaler()

    # X and X_test are images, convert to flat matrices
    X_flat = list(X)
    X_flat = np.nan_to_num(X_flat)
    scaler.fit(X_flat)
    X_flat = np.nan_to_num(scaler.transform(X_flat))

    X_test_flat = list(X_test)
    X_test_flat = np.nan_to_num(X_test_flat)
    X_test_flat = np.nan_to_num(scaler.transform(X_test_flat))

    y = y.astype('category').cat.codes.values
    y_test = y_test.astype('category').cat.codes.values

    # number of trees
    n_estimators = [int(x) for x in np.linspace(start = 40, stop = 2500, num = 40)]
    criterion = ['gini', 'entropy', 'log_loss']
    # number of features to take into account at every split
    max_features = ['log2', 'sqrt']
    # max number of levels in one tree
    max_depth = [int(x) for x in np.linspace(10, 110, num = 20)]
    max_depth.append(None)
    # min samples needed to split a node
    min_samples_split = [2, 3, 4, 5, 10]
    # min samples required at each leaf node
    min_samples_leaf = [1, 2, 3, 4]
    # method of selecting samples for training each tree
    bootstrap = [True, False]

    random_grid = {'n_estimators': n_estimators,
                'max_features': max_features,
                'max_depth': max_depth,
                'min_samples_split': min_samples_split,
                'min_samples_leaf': min_samples_leaf,
                'bootstrap': bootstrap,
                'criterion': criterion}
    print(random_grid)

    # random forest classifier using Scikit-learn package
    clf = RandomForestClassifier()
    rf_random = RandomizedSearchCV(estimator = clf, param_distributions = random_grid, n_iter = 100, cv = 3, verbose=2, random_state=random_seed, n_jobs = -1)
    rf_random.fit(X_flat, y)

    print("train arrays img", X_flat[:10])
    print("train lab", y)

    print(rf_random.best_estimator_)
    print(rf_random.best_params_)

    rf_classifier = rf_random.best_estimator_

    rf_classifier.fit(X_flat, y)

    y_pred = rf_classifier.predict(X_test_flat)

    print("accuracy:", metrics.accuracy_score(y_test, y_pred))
    print("balanced accuracy:", metrics.balanced_accuracy_score(y_test, y_pred))
    print("f1:", metrics.f1_score(y_test, y_pred))
    accuracy = metrics.balanced_accuracy_score(y_test, y_pred)
    sensitivity = metrics.recall_score(y_test, y_pred)
    conf_matrix = confusion_matrix(y_test, y_pred).ravel()
    tn, fp, fn, tp = conf_matrix
    specificity = tn / (tn+fp)

    print(classification_report(y_test, y_pred))

    coefs = rf_classifier.feature_importances_
    print(coefs)

    return rf_classifier, y_pred, accuracy, sensitivity, specificity, conf_matrix, coefs


from bs4 import BeautifulSoup
import requests
import pickle

r  = requests.get("https://www.fifa.com/fifa-world-ranking/associations/index.html")
data = r.text
soup = BeautifulSoup(data)
mydivs = soup.find("div", {"class": "ranking-teamlist"})
team_links = []
links = mydivs.find_all("a")

country_history_rankings = {}
for link in links:
    print(link)
    country_name = link.find("img").get("alt")
    r  = requests.get("https://www.fifa.com"+link.get("href"))
    data = r.text
    soup = BeautifulSoup(data)
    ranking_table = soup.find_all("table",{"class":"table tbl-ranking table-striped"})
    ranking_table = ranking_table[1]
    ranks = ranking_table.find_all("td",{"class":"tbl-rank"})
    years = ranking_table.select("td.tbl-deliveryyear > span")
    match_played = ranking_table.select("td.tbl-matchplayed > span")
    wins = ranking_table.select("td.tbl-matchplayed-win > span")

    match_pl = {}
    for i,y in enumerate(years):
      y = y.get_text()
      mp = int(match_played[i].get_text())
      win = int(wins[i].get_text())
      if y not in match_pl:
         match_pl[y] = {"mp":mp,"win":win}
      else:
         obj = match_pl[y]
         obj["mp"] = obj["mp"]+mp
         obj["win"] = obj["win"]+win

    rank_year = {}
    for i in range(0,len(ranks)):
        rank = ranks[i].find("span").get_text()
        year = years[i].get_text()
        rank_year[year] = rank

    country_history_rankings[country_name] = {"rankings":rank_year,"wins":match_pl}

pickle.dump(country_history_rankings,open("fifa_rankings_history.pkl","wb"))
print(country_history_rankings)


"""
Test to get attendance with valid employee_id, month and year
"""
import datetime

import pytest
from django_swagger_utils.utils.test_utils import TestUtils

from hr_core.tests.factories.models import EmployeeFactory
from hr_core.tests.factories.models import PresentAttendanceFactory
from hr_core.tests.factories.models import SinglePunchInAbsentAttendanceFactory
from . import APP_NAME, OPERATION_NAME, REQUEST_METHOD, URL_SUFFIX

class TestCase01GetFullMonthStatsAPITestCase(TestUtils):
    APP_NAME = APP_NAME
    OPERATION_NAME = OPERATION_NAME
    REQUEST_METHOD = REQUEST_METHOD
    URL_SUFFIX = URL_SUFFIX
    SECURITY = {'oauth': {'scopes': ['read']}}

    @pytest.mark.django_db
    def test_case(self, snapshot):
        # Arrange
        body = {}
        path_params = {"employee_id": '0'}
        test_date = datetime.datetime(year=2023, month=2, day=20)
        test_time_delta = datetime.timedelta(hours=9)
        query_params = {'month': test_date.month, 'year': test_date.year}
        headers = {}
        employee = EmployeeFactory()

        present = PresentAttendanceFactory(employee=employee,
                                           clock_in_datetime=test_date,
                                           clock_out_datetime=test_date + test_time_delta
                                           )

        absent = SinglePunchInAbsentAttendanceFactory(employee=employee,
                                                      clock_in_datetime=test_date)
        # Act and Assert
        response = self.make_api_call(body=body,
                                      path_params=path_params,
                                      query_params=query_params,
                                      headers=headers,
                                      snapshot=snapshot)


'''
AoC 2018 Day 22
'''

import numpy as np          # For the map and numeric manipulations

# Real data
map_depth = 8787
target = (725, 10)      # (Y coord first)

# Sample data - answer should be 114
# map_depth = 510
# target = (10, 10)       # (Y coord first)

def calc_geological_index(cx, cy):
    global map_depth, target, erosion_levels, geological_levels, region_types

    xmul = 16807
    ymul = 48271

    if (cy, cx) == (0, 0):
        geological_levels[cy, cx] = 0

    elif (cy, cx) == map_depth:
        geological_levels[cy, cx] = 0

    elif cx == 0:
        geological_levels[cy, cx] = cy*ymul

    elif cy == 0:
        geological_levels[cy, cx] = cx*xmul

    else:
        if erosion_levels[cy-1, cx] == -1:
            calc_erosion_level(cx, cy-1)

        if erosion_levels[cy, cx-1] == -1:
            calc_erosion_level(cx-1, cy)

        geological_levels[cy, cx] = erosion_levels[cy, cx-1] * erosion_levels[cy-1, cx]

    # return geological_levels[cy, cx]

def calc_erosion_level(cx, cy):
    global map_depth, target, erosion_levels, geological_levels, region_types

    mod_val = 20183

    if geological_levels[cy, cx] == -1:
        calc_geological_index(cx, cy)

    erosion_levels[cy, cx] = (geological_levels[cy, cx] + map_depth) % mod_val
    region_types[cy, cx] = erosion_levels[cy, cx] % 3

    # return erosion_levels[cy, cx]

def part1():
    global map_depth, target, erosion_levels, geological_levels, region_types

    # Setup map based on Input Data
    cy, cx = target
    erosion_levels = np.ones([cy+2, cx+2], dtype=np.int64)
    geological_levels = np.ones([cy+2, cx+2], dtype=np.int64)
    region_types = np.ones([cy+2, cx+2], dtype=np.int64)
    erosion_levels *= -1
    geological_levels *= -1
    region_types *= -1

    # Let's populate the map between the origin and the target
    # Add the risk as we go
    risk = 0
    for map_y in range(cy+1):
        for map_x in range(cx+1):
            calc_erosion_level(map_x, map_y)
            risk += region_types[map_y, map_x]

    risk -= region_types[0,0]
    risk -= region_types[cy, cx]

    # Print the risk
    print(f"Part 1: Risk = {risk}")

    # Just to see it, let's print the map
    # print_map()

def print_map():
    global target, region_types

    cy, cx = target
    for map_y in range(cy+2):
        line = []
        for map_x in range(cx+2):
            if map_y == 0 and map_x == 0:
                line.append("M")
            elif map_y == cy and map_x == cx:
                line.append("T")
            elif region_types[map_y, map_x] == 0:
                line.append(".")
            elif region_types[map_y, map_x] == 1:
                line.append("=")
            elif region_types[map_y, map_x] == 2:
                line.append("|")
        print("".join(line))
    print()

if __name__ == "__main__":
    part1()


"""
Created on Apr 30, 2014

@author: ygor
"""

from .core import liblevenshtein
import numpy as np

def normalized_sequential_levenshtein(list_s, list_t, *args, **kwargs):
    return similarity(len(list_s), len(list_t), sequential_levenshtein(list_s, list_t, *args, **kwargs))

def parallel_levenshtein(list_s, list_t, *args, **kwargs):
    s_len = len(list_s)
    t_len = len(list_t)
    equality_dict = kwargs.get('equality_dict', None);
    set_s = kwargs.get('set_s', None);
    set_t = kwargs.get('set_t', None);

    # Call function
    if(equality_dict != None):
        s_set_len = len(set_s)
        t_set_len = len(set_t)

        return liblevenshtein.parallel_levenshtein(list_s, s_len, list_t, t_len, equality_dict, set_s, s_set_len, set_t, t_set_len)

    return liblevenshtein.parallel_levenshtein(list_s, s_len, list_t, t_len)

def sequential_levenshtein(list_s, list_t, *args, **kwargs):
    s_len = len(list_s)
    t_len = len(list_t)
    equality_dict = kwargs.get('equality_dict', None);
    set_s = kwargs.get('set_s', None);
    set_t = kwargs.get('set_t', None);

    # Call function
    if(equality_dict != None):
        s_set_len = len(set_s)
        t_set_len = len(set_t)

        return liblevenshtein.sequential_levenshtein(list_s, s_len, list_t, t_len, equality_dict, set_s, s_set_len, set_t, t_set_len)

    return liblevenshtein.sequential_levenshtein(list_s, s_len, list_t, t_len)

def cos_similarity(list_s, list_t):
    bag_s = list_s.to_bag()
    bag_t = list_t.to_bag()

    set_s = list_s.to_set()
    set_t = list_t.to_set()

    keys = set_s.union(set_t)

    array_s = np.zeros(len(keys))
    array_t = np.zeros(len(keys))

    rank = 0

    for k in keys:
        if k in bag_s:
            array_s[rank] = bag_s.get(k)
        if k in bag_t:
            array_t[rank] = bag_t.get(k)
        rank = rank + 1

    dotted = np.dot(array_s, np.transpose(array_t))
    norms = np.linalg.norm(array_s) * np.linalg.norm(array_t)

    return dotted/norms

def jaccard_similarity(list_s, list_t):
    set_s = list_s.to_set()
    set_t = list_t.to_set()

    len_insersection = len(set_s.intersection(set_t))
    len_union = len(set_s.union(set_t))

    dis = float(len_insersection) / float(len_union)

    return dis

def dissimilarity(s_len, t_len, distance):
    return float(distance)/max(s_len, t_len)

def similarity(s_len, t_len, distance):
    return 1 - dissimilarity(s_len, t_len, distance)


import numpy
from numpy.linalg import inv, slogdet
from scipy.special import logsumexp

class GMM:

    def __init__(self, components, X, log_prior, prior_weight, small_number = 1e-6, log_likelihood_tolerance = 1e-3, max_iterations = 1000):
        self.__components = components  # number of components
        self.__X = X  # list of samples, each sample is a #bins by #features matrix
        self.__log_prior = log_prior  # log probability
        self.__prior_weight = prior_weight
        self.__small_number = small_number
        self.__log_likelihood_tolerance = log_likelihood_tolerance
        self.__max_iterations = max_iterations

    def __get_x(self):
        '''
        self.__x: samples * features
        '''
        self.__x = numpy.array([numpy.mean(X, axis = 0) for X in self.__X], dtype = numpy.float64)

    def __initialize_log_gaussian_density(self):
        self.__log_gaussian_density = numpy.empty(shape = (self.__samples, self.__components), dtype = numpy.float64)

    def __construct_mapping(self):
        self.__mapping = numpy.array([index for index, X in enumerate(self.__X) for _ in X], dtype = numpy.int64)

    def __flatten_X(self):
        self.__X = numpy.concatenate(self.__X, axis = 0)

    def __initialize_responsibility(self):
        self.__responsibility = numpy.copy(self.__prior)
        self.log_responsibility = numpy.copy(self.__log_prior)

    def __update_mu(self):
        weight = self.__responsibility[self.__mapping]
        self.__mu = weight.T @ self.__X / (numpy.sum(weight, axis = 0, keepdims = True).T)

    def __update_sigma(self):
        self.__sigma = numpy.empty(shape = (self.__components, self.__features, self.__features), dtype = numpy.float64)
        weight = self.__responsibility[self.__mapping]
        for component in range(self.__components):
            temp = self.__X - self.__mu[component]
            self.__sigma[component] = temp.T * weight[:, component] @ temp / numpy.sum(weight[ : , component])
            self.__sigma[component][numpy.diag_indices(self.__features)] += self.__small_number

    def __update_pi(self):
        self.__log_pi = numpy.log(self.__responsibility + self.__prior_weight * self.__prior) - numpy.log(1.0 + self.__prior_weight)
        self.__pi = numpy.exp(self.__log_pi)

    def __calculate_log_gaussian_density(self):
        for component in range(self.__components):
            temp = self.__x - self.__mu[component]
            self.__log_gaussian_density[:, component] = (
                -0.5 * (
                    numpy.sum(temp @ inv(self.__sigma[component]) * temp, axis = 1)
                    + self.__features * numpy.log(2.0 * numpy.pi)
                    + slogdet(self.__sigma[component])[1]
                )
            )

    def __update_responsibility(self):
        self.__calculate_log_gaussian_density()
        # log(pi) + log(gaussian density)
        self.log_responsibility = self.__log_pi + self.__log_gaussian_density
        # log(posterior)
        self.log_responsibility -= logsumexp(self.log_responsibility, axis = 1, keepdims = True)
        # posterior
        self.__responsibility = numpy.exp(self.log_responsibility)
        self.__responsibility += numpy.finfo(numpy.float64).eps
        self.__responsibility /= numpy.sum(self.__responsibility, axis = 1, keepdims = True)

    def __calculate_log_likelihood(self):
        log_likelihood = (
            numpy.sum(logsumexp(self.__log_pi[self.__mapping] + self.__log_gaussian_density[self.__mapping], axis = 1))
            - self.__prior_weight * numpy.sum(self.__prior[self.__mapping] * (self.__log_prior[self.__mapping] - self.__log_pi[self.__mapping]))
        )
        return log_likelihood

    def main(self):
        self.__prior = numpy.exp(self.__log_prior)
        self.__get_x()  # self.__x
        self.__samples, self.__features = self.__x.shape
        self.__initialize_log_gaussian_density()  # self.__log_gaussian_density
        self.__construct_mapping()  # self.__mapping
        self.__flatten_X()
        self.__initialize_responsibility()  # self.__responsibility self.log_responsibility
        lower_log_likelihood = -numpy.inf
        iterations = 0
        while iterations < self.__max_iterations:
            self.__update_mu()  # m step
            self.__update_sigma()  # m step
            self.__update_pi()  # m step
            self.__update_responsibility()  # e step, update posterior distribution
            log_likelihood = self.__calculate_log_likelihood()  # log likelihood
            iterations += 1
            if (log_likelihood - lower_log_likelihood) < self.__log_likelihood_tolerance:
                break
            lower_log_likelihood = log_likelihood


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import random
import time
import torch
from torch.utils.data import DataLoader
import numpy as np
from hparam import hparam as hp
from data_load import SpeakerDatasetTIMIT, SpeakerDatasetTIMITPreprocessed
from speech_embedder_net import SpeechEmbedder, GE2ELoss, get_centroids, get_cossim

def test(model_path):
    sequence = []
    if hp.data.data_preprocessed:
        test_dataset = SpeakerDatasetTIMITPreprocessed()
    else:
        test_dataset = SpeakerDatasetTIMIT()
    test_loader = DataLoader(test_dataset, batch_size=1, shuffle=True, num_workers=hp.test.num_workers,
                             drop_last=True)

    embedder_net = SpeechEmbedder()
    embedder_net.load_state_dict(torch.load(model_path))
    embedder_net.eval()

    device = torch.device(hp.device)
    count = 0
    embeddings = []
    devector = []
    for e in range(hp.test.epochs):
        print("hp.test.epochs",hp.test.epochs)
        for batch_id, mel_db_batch in enumerate(test_loader):
            #print("mel_db_batch.shape",batch_id,mel_db_batch.shape)   #(1,10,160,40)
            assert hp.test.M % 2 == 0
            test_batch = mel_db_batch
            test_batch = torch.reshape(test_batch, (
            hp.test.N * hp.test.M, test_batch.size(2), test_batch.size(3)))
            #print("test_batch.shape",test_batch.shape)    #(10,160,40)

            enrollment_embeddings = embedder_net(test_batch)
            #print("enrollment_embeddings.shape", enrollment_embeddings.shape)  # (10,256)
            # enrollment_embeddings = torch.reshape(enrollment_embeddings,(hp.test.N, hp.test.M, enrollment_embeddings.size(1)))

            embedding = enrollment_embeddings.detach().numpy()
            embeddings.append(embedding)
            #print('embedding.shape', type(embedding), embedding.shape)  # (10,256)

            devector = np.concatenate(embeddings, axis=0)
            count = count + 1
    np.save('/run/media/rice/DATA/speakerdvector.npy', devector)

if __name__ == "__main__":
    test(hp.model.model_path)


import matplotlib.pyplot as plt
import numpy as np
from typing import List

def plot_training_results(rewards_history: List,
                          running_rewards_history: List,
                          steps_history: List,
                          loss_history: List,
                          wallclock_history: List,
                          save_dir: str = None):

    fig, ((ax1, ax2, ax3), (ax4, ax5, ax6), (ax7, ax8, ax9)) = plt.subplots(nrows=3, ncols=3, figsize=(16, 12))
    fig.suptitle("Training Results", fontsize=16)

    # EPISODES
    num_episodes = len(rewards_history)

    # Episodes vs rewards
    ax1.plot(np.arange(num_episodes), rewards_history, color='grey', alpha=0.3, linestyle='-', label="episode reward")
    ax1.plot(np.arange(num_episodes), running_rewards_history, color='green', label="moving average reward")
    ax1.set_xlabel("Number of episodes")
    ax1.set_ylabel("Episode reward")
    ax1.legend()
    ax1.grid()

    # Episodes vs loss
    ax4.plot(np.arange(num_episodes), loss_history, color='green')
    ax4.set_xlabel("Number of episodes")
    ax4.set_ylabel("Episode loss")
    ax4.grid()

    # Epochs vs steps per epoch
    ax7.plot(np.arange(num_episodes), steps_history, color='green')
    ax7.set_xlabel("Number of episodes")
    ax7.set_ylabel("Episode duration (steps)")
    ax7.grid()

    # STEPS
    cumulative_steps = list(np.cumsum(steps_history))

    # Steps vs rewards
    ax2.plot(cumulative_steps, rewards_history, color='grey', alpha=0.3, linestyle='-', label="episode reward")
    ax2.plot(cumulative_steps, running_rewards_history, color='green', label="moving average reward")
    ax2.set_xlabel("Number of steps")
    ax2.set_ylabel("Episode reward")
    ax2.legend()
    ax2.grid()

    # Steps vs loss
    ax5.plot(cumulative_steps, loss_history, color='green')
    ax5.set_xlabel("Number of steps")
    ax5.set_ylabel("Episode loss")
    ax5.grid()

    # TIME

    # Wallclock vs rewards
    ax3.plot(wallclock_history, rewards_history, color='grey', alpha=0.3, linestyle='-', label="episode reward")
    ax3.plot(wallclock_history, running_rewards_history, color='green', label="moving average reward")
    ax3.set_xlabel("Wallclock time (seconds)")
    ax3.set_ylabel("Episode reward")
    ax3.legend()
    ax3.grid()

    # Wallclock vs loss
    ax6.plot(wallclock_history, loss_history, color='green')
    ax6.set_xlabel("Wallclock time (seconds)")
    ax6.set_ylabel("Episode loss")
    ax6.grid()

    # Turn off unused plots
    ax8.set_axis_off()
    ax9.set_axis_off()

    # Save
    if save_dir:
        plt.savefig(save_dir)

    # Show plots
    plt.tight_layout()
    plt.show()


from kivy.lang.builder import Builder
from kivy.uix.label import Label

Builder.load_string('''
<Divider>:
    height: '1sp'
    size_hint_y: None
    color: (0.5, 0.5, 0.5, 1)
    canvas.before:
        Color:
            rgba: self.color
        Rectangle:
            pos: self.pos
            size: self.size
''')

class Divider(Label):

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


#!/bin/python3

import json
import requests
import logging
from datetime import datetime, timedelta

def configure_logging():
    logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)

def _set_headers(username: str = None, api_key: str = None):
    headers = {'Content-Type': 'application/json'}
    if username: headers['Authorization'] = f'Basic {username}'
    if api_key: headers['PRIVATE-TOKEN'] = f'{api_key}'
    return headers

def _set_uri():
    return '{gitlab}/api/v4'

def get_access_token():
    return ''

def process_api_request(url: str, verb: str, headers: dict, data: dict = None, params: dict = None):
    try:
        if data: r = getattr(requests, verb.lower())(url,headers=headers,data=json.dumps(data))
        elif params: r = getattr(requests, verb.lower())(url,headers=headers,params=params)
        else: r = getattr(requests, verb.lower())(url,headers=headers)

        r.raise_for_status()
    except Exception as e:
        logging.error(f'An error occured executing the API call: {e}')

    try:
        return r.json()
    except Exception as e:
        logging.error(f'An error occured loading the content: {e}')
        return None

def get_jobs(token, page, project_id):
    data = {
        'page' : page,
        'per_page' : 50
    }
    # List project jobs | https://docs.gitlab.com/ee/api/jobs.html#list-project-jobs
    return process_api_request(f'https://{_set_uri()}/projects/{project_id}/jobs', 'GET', _set_headers(api_key=token), params=data)

def get_job_count(token, page, project_id):
    count = get_jobs(token, page, project_id)
    if len(count) > 0: return True
    return False

def delete_artifacts(token, project_id, job_id):
    # Delete job artifacts | https://docs.gitlab.com/ee/api/job_artifacts.html#delete-artifacts
    try:
        process_api_request(f'https://{_set_uri()}/projects/{project_id}/jobs/{job_id}/artifacts', 'DELETE', _set_headers(api_key=token))
    except Exception as e:
        logging.error(f'An error occured loading the content: {e}')
    return False

def main():
    configure_logging()
    token=get_access_token()
    project_id='254'

    logging.info(f"Start cleaning project: {project_id}")

    page_count = 1
    while get_job_count(token, page_count, project_id):
        jobs = get_jobs(token, page_count, project_id)

        for entry in jobs:
            create_date = datetime.strptime(entry["created_at"], '%Y-%m-%dT%H:%M:%S.%fZ')
            time_between_insertion = datetime.now() - create_date

            if time_between_insertion.days>=30 and len(entry["artifacts"]) > 0:
                #logging.info(f"Remove job artifacts for job {entry['id']}")
                delete_artifacts(token,project_id,entry["id"])
        page_count +=1

    logging.info(f"Finished cleaning project {project_id}")

if __name__ == "__main__":
    main()