import numpy as np

import pandas as pd

import gc

import lightgbm as lgb

from sklearn.model_selection import train_test_split

from sklearn.preprocessing import LabelEncoder

metadata_dtype = {'site_id':"uint8",'building_id':'uint16','square_feet':'float32','year_built':'float32','floor_count':"float16"}

weather_dtype = {"site_id":"uint8",'air_temperature':"float16",'cloud_coverage':"float16",'dew_temperature':"float16",'precip_depth_1_hr':"float16",

                 'sea_level_pressure':"float32",'wind_direction':"float16",'wind_speed':"float16"}

train_dtype = {'meter':"uint8",'building_id':'uint16'}

weather_train = pd.read_csv("../input/ashrae-energy-prediction/weather_train.csv", parse_dates=['timestamp'], dtype=weather_dtype)

weather_test = pd.read_csv("../input/ashrae-energy-prediction/weather_test.csv", parse_dates=['timestamp'], dtype=weather_dtype)

metadata = pd.read_csv("../input/ashrae-energy-prediction/building_metadata.csv", dtype=metadata_dtype)

train = pd.read_csv("../input/ashrae-energy-prediction/train.csv", parse_dates=['timestamp'], dtype=train_dtype)

test = pd.read_csv("../input/ashrae-energy-prediction/test.csv", parse_dates=['timestamp'], usecols=['building_id','meter','timestamp'], dtype=train_dtype)

#from sklearn.utils import resample

#train_size = 1_000_000

#test_size = 1_000_000

#seed = 947

#train = resample(train, replace=False, n_samples=train_size, random_state=seed)

#test = resample(test, replace=False, n_samples=test_size, random_state=seed)
train['meter'].replace({0:"Electricity",1:"ChilledWater",2:"Steam",3:"HotWater"},inplace=True)

test['meter'].replace({0:"Electricity",1:"ChilledWater",2:"Steam",3:"HotWater"},inplace=True)

metadata["floor_count"].fillna(int(metadata["floor_count"].mean()), inplace=True)

for df in [train, test]:

    df['Month'] = df['timestamp'].dt.month.astype("uint8")

    df['DayOfMonth'] = df['timestamp'].dt.day.astype("uint8")

    df['DayOfWeek'] = df['timestamp'].dt.dayofweek.astype("uint8")

    df['Hour'] = df['timestamp'].dt.hour.astype("uint8")

train['meter_reading'] = np.log1p(train['meter_reading'])

metadata['primary_use'].replace({"Healthcare":"Other","Parking":"Other","Warehouse/storage":"Other","Manufacturing/industrial":"Other",

                                "Retail":"Other","Services":"Other","Technology/science":"Other","Food sales and service":"Other",

                                "Utility":"Other","Religious worship":"Other"},inplace=True)

metadata['square_feet'] = np.log1p(metadata['square_feet'])

metadata['year_built'].fillna(int(metadata["year_built"].mean()), inplace=True)

metadata['year_built'] = metadata['year_built'].astype('int16')

train = pd.merge(train,metadata,on='building_id',how='left')

test  = pd.merge(test,metadata,on='building_id',how='left')

gc.collect()

train = pd.merge(train,weather_train,on=['site_id','timestamp'],how='left')

test  = pd.merge(test,weather_test,on=['site_id','timestamp'],how='left')

gc.collect()
# Save space

for df in [train,test]:

    df['square_feet'] = df['square_feet'].astype('float16')

# Fill NA

cols = ['air_temperature','cloud_coverage','dew_temperature','precip_depth_1_hr','sea_level_pressure','wind_speed', "wind_direction"]

for col in cols:

    train[col].fillna(train[col].mean(),inplace=True)

    test[col].fillna(test[col].mean(),inplace=True)

# Drop nonsense entries

# As per the discussion in the following thread, https://www.kaggle.com/c/ashrae-energy-prediction/discussion/117083, there is some discrepancy in the meter_readings for different ste_id's and buildings. It makes sense to delete them

idx_to_drop = list((train[(train['site_id'] == 0) & (train['timestamp'] < "2016-05-21 00:00:00")]).index)

train.drop(idx_to_drop,axis='rows',inplace=True)

# dropping all the electricity meter readings that are 0, after considering them as anomalies.

idx_to_drop = list(train[(train['meter'] == "Electricity") & (train['meter_reading'] == 0)].index)

train.drop(idx_to_drop,axis='rows',inplace=True)

mean_temperature_per_site = train.groupby("site_id")["air_temperature"].mean()

train["mean_temperature_per_site"] = train["site_id"].map(mean_temperature_per_site)

test["mean_temperature_per_site"] = test["site_id"].map(mean_temperature_per_site)

number_unique_meter_per_building = train.groupby('building_id')['meter'].nunique()

train['number_unique_meter_per_building'] = train['building_id'].map(number_unique_meter_per_building)

mean_meter_reading_per_building = train.groupby('building_id')['meter_reading'].mean()

train['mean_meter_reading_per_building'] = train['building_id'].map(mean_meter_reading_per_building)

median_meter_reading_per_building = train.groupby('building_id')['meter_reading'].median()

train['median_meter_reading_per_building'] = train['building_id'].map(median_meter_reading_per_building)

std_meter_reading_per_building = train.groupby('building_id')['meter_reading'].std()

train['std_meter_reading_per_building'] = train['building_id'].map(std_meter_reading_per_building)

mean_meter_reading_on_year_built = train.groupby('year_built')['meter_reading'].mean()

train['mean_meter_reading_on_year_built'] = train['year_built'].map(mean_meter_reading_on_year_built)

median_meter_reading_on_year_built = train.groupby('year_built')['meter_reading'].median()

train['median_meter_reading_on_year_built'] = train['year_built'].map(median_meter_reading_on_year_built)

std_meter_reading_on_year_built = train.groupby('year_built')['meter_reading'].std()

train['std_meter_reading_on_year_built'] = train['year_built'].map(std_meter_reading_on_year_built)

mean_meter_reading_per_meter = train.groupby('meter')['meter_reading'].mean()

train['mean_meter_reading_per_meter'] = train['meter'].map(mean_meter_reading_per_meter)

median_meter_reading_per_meter = train.groupby('meter')['meter_reading'].median()

train['median_meter_reading_per_meter'] = train['meter'].map(median_meter_reading_per_meter)

std_meter_reading_per_meter = train.groupby('meter')['meter_reading'].std()

train['std_meter_reading_per_meter'] = train['meter'].map(std_meter_reading_per_meter)

mean_meter_reading_per_primary_usage = train.groupby('primary_use')['meter_reading'].mean()

train['mean_meter_reading_per_primary_usage'] = train['primary_use'].map(mean_meter_reading_per_primary_usage)

median_meter_reading_per_primary_usage = train.groupby('primary_use')['meter_reading'].median()

train['median_meter_reading_per_primary_usage'] = train['primary_use'].map(median_meter_reading_per_primary_usage)

std_meter_reading_per_primary_usage = train.groupby('primary_use')['meter_reading'].std()

train['std_meter_reading_per_primary_usage'] = train['primary_use'].map(std_meter_reading_per_primary_usage)

mean_meter_reading_per_site_id = train.groupby('site_id')['meter_reading'].mean()

train['mean_meter_reading_per_site_id'] = train['site_id'].map(mean_meter_reading_per_site_id)

median_meter_reading_per_site_id = train.groupby('site_id')['meter_reading'].median()

train['median_meter_reading_per_site_id'] = train['site_id'].map(median_meter_reading_per_site_id)

std_meter_reading_per_site_id = train.groupby('site_id')['meter_reading'].std()

train['std_meter_reading_per_site_id'] = train['site_id'].map(std_meter_reading_per_site_id)

test['number_unique_meter_per_building'] = test['building_id'].map(number_unique_meter_per_building)

test['mean_meter_reading_per_building'] = test['building_id'].map(mean_meter_reading_per_building)

test['median_meter_reading_per_building'] = test['building_id'].map(median_meter_reading_per_building)

test['std_meter_reading_per_building'] = test['building_id'].map(std_meter_reading_per_building)

test['mean_meter_reading_on_year_built'] = test['year_built'].map(mean_meter_reading_on_year_built)

test['median_meter_reading_on_year_built'] = test['year_built'].map(median_meter_reading_on_year_built)

test['std_meter_reading_on_year_built'] = test['year_built'].map(std_meter_reading_on_year_built)

test['mean_meter_reading_per_meter'] = test['meter'].map(mean_meter_reading_per_meter)

test['median_meter_reading_per_meter'] = test['meter'].map(median_meter_reading_per_meter)

test['std_meter_reading_per_meter'] = test['meter'].map(std_meter_reading_per_meter)

test['mean_meter_reading_per_primary_usage'] = test['primary_use'].map(mean_meter_reading_per_primary_usage)

test['median_meter_reading_per_primary_usage'] = test['primary_use'].map(median_meter_reading_per_primary_usage)

test['std_meter_reading_per_primary_usage'] = test['primary_use'].map(std_meter_reading_per_primary_usage)

test['mean_meter_reading_per_site_id'] = test['site_id'].map(mean_meter_reading_per_site_id)

test['median_meter_reading_per_site_id'] = test['site_id'].map(median_meter_reading_per_site_id)

test['std_meter_reading_per_site_id'] = test['site_id'].map(std_meter_reading_per_site_id)

for df in [train, test]:

    df['mean_temperature_per_site'] = df['mean_temperature_per_site'].astype("float16")

    df['mean_meter_reading_per_building'] = df['mean_meter_reading_per_building'].astype("float16")

    df['median_meter_reading_per_building'] = df['mean_meter_reading_per_building'].astype("float16")

    df['std_meter_reading_per_building'] = df['std_meter_reading_per_building'].astype("float16")

    df['mean_meter_reading_on_year_built'] = df['mean_meter_reading_on_year_built'].astype("float16")

    df['median_meter_reading_on_year_built'] = df['median_meter_reading_on_year_built'].astype("float16")

    df['std_meter_reading_on_year_built'] = df['std_meter_reading_on_year_built'].astype("float16")

    df['mean_meter_reading_per_meter'] = df['mean_meter_reading_per_meter'].astype("float16")

    df['median_meter_reading_per_meter'] = df['median_meter_reading_per_meter'].astype("float16")

    df['std_meter_reading_per_meter'] = df['std_meter_reading_per_meter'].astype("float16")

    df['mean_meter_reading_per_primary_usage'] = df['mean_meter_reading_per_primary_usage'].astype("float16")

    df['median_meter_reading_per_primary_usage'] = df['median_meter_reading_per_primary_usage'].astype("float16")

    df['std_meter_reading_per_primary_usage'] = df['std_meter_reading_per_primary_usage'].astype("float16")

    df['mean_meter_reading_per_site_id'] = df['mean_meter_reading_per_site_id'].astype("float16")

    df['median_meter_reading_per_site_id'] = df['median_meter_reading_per_site_id'].astype("float16")

    df['std_meter_reading_per_site_id'] = df['std_meter_reading_per_site_id'].astype("float16")

    df['number_unique_meter_per_building'] = df['number_unique_meter_per_building'].astype('uint8')

gc.collect()
train.drop('timestamp',axis=1,inplace=True)

test.drop('timestamp',axis=1,inplace=True)

le = LabelEncoder()

train['meter']= le.fit_transform(train['meter']).astype("uint8")

test['meter']= le.fit_transform(test['meter']).astype("uint8")

train['primary_use']= le.fit_transform(train['primary_use']).astype("uint8")

test['primary_use']= le.fit_transform(test['primary_use']).astype("uint8")

print (train.shape, test.shape)

# Let's check the correlation between the variables and eliminate the one's that have high correlation

# Threshold for removing correlated variables

threshold = 0.90

# Absolute value correlation matrix

corr_matrix = train.corr().abs()

# Upper triangle of correlations

upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))

# Select columns with correlations above threshold

to_drop = [column for column in upper.columns if any(upper[column] > threshold)]

print('There are %d columns to remove.' % (len(to_drop)))

print ("Following columns can be dropped {}".format(to_drop))

train.drop(to_drop,axis=1,inplace=True)

test.drop(to_drop,axis=1,inplace=True)

y = train['meter_reading']

train.drop('meter_reading',axis=1,inplace=True)

categorical_cols = ['building_id','Month','meter','Hour','primary_use','DayOfWeek','DayOfMonth']
x_train,x_test,y_train,y_test = train_test_split(train,y,test_size=0.05,random_state=573)

print (x_train.shape)

print (x_test.shape)

lgb_train = lgb.Dataset(x_train, y_train, categorical_feature=categorical_cols)

lgb_test = lgb.Dataset(x_test, y_test, categorical_feature=categorical_cols)

del x_train, x_test , y_train, y_test

params = {'feature_fraction': 0.8,

          'bagging_fraction': 0.7,

          'objective': 'regression',

          'max_depth': -1,

          'learning_rate': 0.1,

          "boosting_type": "gbdt",

          "bagging_seed": 321,

          "metric": 'rmse',

          "verbosity": -1,

          'reg_alpha': 1,

          'reg_lambda': 2,

          'random_state': 123,

          'num_leaves': 70

         }

reg = lgb.train(params, lgb_train, num_boost_round=3000, valid_sets=[lgb_train, lgb_test], early_stopping_rounds=100, verbose_eval=100)

del train, y, lgb_train

predictions = []

step = 50000

for i in range(0, len(test), step):

    predictions.extend(np.expm1(reg.predict(test.iloc[i: min(i+step, len(test)), :], num_iteration=reg.best_iteration)))

Submission = pd.DataFrame(test.index,columns=['row_id'])

Submission['meter_reading'] = predictions

Submission['meter_reading'].clip(lower=0,upper=None,inplace=True)

Submission.to_csv("lgbm.csv",index=None)


#! /usr/bin/env python

from typing import List
from random import randint

def main() -> None:

    numbers: List[int] = [ randint(0,9) ] * 10000000

    for number in numbers:
        number = number ** 1000

if __name__ == '__main__':
    main()


import logging

from fastapi import APIRouter, Depends, HTTPException

from .domain.borrow import Borrow
from .repository.borrow_sql_repository import BorrowSQLRepository
from .repository.reader_sql_repository import ReaderSQLRepository
from .repository.book_sql_repository import BooksSQLRepository

from .dependencies import get_borrow_repository, get_reader_repository, get_book_repository

log = logging.getLogger(__name__)
router = APIRouter()

@router.post("/v1/borrows")
async def add_borrow(borrow: Borrow, borrow_repo: BorrowSQLRepository = Depends(get_borrow_repository),
                     reader_repo: ReaderSQLRepository = Depends(get_reader_repository),
                     book_repo: BooksSQLRepository = Depends(get_book_repository)):
    is_reader_exist = await reader_repo.is_reader_exist(borrow.reader_id)
    is_book_exist = await book_repo.is_book_exist(borrow.book_id)

    if is_book_exist and is_reader_exist:
        book_borrowed = await borrow_repo.book_borrowed(borrow.book_id)
        if book_borrowed:
            if book_borrowed.reader_id != borrow.reader_id:
                raise HTTPException(status_code=403, detail="Requested book is already borrowed. "
                                                            "Please try with other book.")
            else:
                log.debug("Book is already borrowed by same reader.")
                return
        else:
            return await borrow_repo.add_borrow(borrow)

    raise HTTPException(status_code=404, detail="Requested reader or book does not exist. Please check the data.")

@router.delete("/v1/borrows/{book_id}")
async def del_borrow(book_id: int, borrow_repo: BorrowSQLRepository = Depends(get_borrow_repository)):
    return await borrow_repo.delete_borrow(book_id)

@router.get("/v1/borrows")
async def get_borrows(borrow_repo: BorrowSQLRepository = Depends(get_borrow_repository)):
    res = await borrow_repo.get_borrows()
    return {"borrows": res}


###
# 20-01-2021
# Hi, here's your problem today. This problem was recently asked by Google:
#
# Given a string with a certain rule: k[string] should be expanded to string k times.
# So for example, 3[abc] should be expanded to abcabcabc. Nested expansions can happen,
# so 2[a2[b]c] should be expanded to abbcabbc.
###
def decode_string(s):
    res = ""
    i = 0
    while i < len(s):
        if s[i].isalpha():
            res += s[i]
            i += 1
            continue

        if s[i].isdigit():
            # get the number
            num = 0
            while s[i].isdigit():
                num = num * 10 + int(s[i])
                i += 1
            # parse string in bracket
            bracket = 1
            i += 1
            innerstr = ""
            while bracket > 0:
                if s[i] == '[':
                    bracket += 1
                if s[i] == ']':
                    bracket -= 1
                innerstr += s[i]
                i += 1
            res = res + num * decode_string(innerstr[:-1])

    return res

print(decode_string('2[a2[b]c]'))


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

from biye.spider.operate import database
from biye.spider.operate import formatfile

def statistic_post():
    """
    判断用户的活跃度，活跃等级为七级, 以用户的发帖数为判断标准
    :return:
    """
    posts = database.select_all('pure_users', 'post_number')
    active = {
        "0-20": 0,
        "20-100": 0,
        "100-500": 0,
        "0.5K-1K": 0,
        "1K-5K": 0,
        "5K-10K": 0,
        "10K+": 0
    }

    for post in posts:
        try:
            if post[0][-1] == '万':
                post_int = float(post[0][:-1]) * 10000
            else:
                post_int = int(post[0])
            if 0 <= post_int < 20:
                active["0-20"] += 1
            elif 20 <= post_int < 100:
                active["20-100"] += 1
            elif 100 <= post_int < 500:
                active["100-500"] += 1
            elif 500 <= post_int < 1000:
                active["0.5K-1K"] += 1
            elif 1000 <= post_int < 5000:
                active["1K-5K"] += 1
            elif 5000 <= post_int < 10000:
                active["5K-10K"] += 1
            else:
                active["10K+"] += 1
        except ValueError as why:
            pass
    return active
revi = statistic_post()
formatfile.dict_to_json(revi, '../../show/data/post.json')


# the class player has attributes mana, creatures, land etc.

import random
from creatures import *
from Button import *

class Player(object):
    def __init__(self,color,name=None,AI=False):
        self.color = color
        self.mana = 0
        self.manaLeft = 0
        self.name = name
        self.isAI = AI

    def __repr__(self):
        return self.name

    def AIMove(self,data):
        assert(data.curPlayer == self)
        # data.curMana
        # first move creatures
            # move wizard
                # use landchange if enemy in sight
                # move to opponent's / blank space
            # move executor
                # if two or more enemies can be killed, transform, kill
                # if 1 enemy in sight, kill
                # move to opponent's / blank space
            # move archer
                # if transform can kill wizard/executor, transform and kill
                # kill
                # move
            # move soldier
                # if path has 4 lands and last move not adjacent to opponent's,
                # transform and move
                # kill
                # move
        # add creatures
            # close to own creatures
            # away from enemies creatures
            # creature created randomly
        (creatures,enemyCreatures,enemyLand,
            availableLand,allLands) = self.getCreatures(data)
        if(data.selectedCreature == None):
            # if(self.mana >= data.player1.mana+15 and data.curMana >= 5):
            #     pos = self.toHelpIslandsPresent(data,allLands)
            #     if(pos != None):
            #         (row,col) = pos
            #         data.squares[row][col] = Square(row,col,None,"bridge")
            #         data.curMana -= 5
            #         return
            if(len(creatures["wizard"]) != 0):
                self.selectCreature(data,random.choice(creatures["wizard"]))
            elif(len(creatures["archer"]) != 0):
                self.selectCreature(data,random.choice(creatures["archer"]))
            elif(len(creatures["executor"]) != 0):
                self.selectCreature(data,random.choice(creatures["executor"]))
            elif(len(creatures["soldier"]) != 0):
                self.selectCreature(data,random.choice(creatures["soldier"]))
            else:
                if(data.curMana >= 5 and len(availableLand) != 0): # choose recruitment
                    (row,col) = random.choice(availableLand)
                    if(self.mana >= data.player1.mana + 15 and data.curMana >= 10):
                        data.creatures[row][col]=Archer(row,col,self.color,data,True)
                        data.curMana -= 10
                    elif(data.curMana < 10):
                        data.creatures[row][col]=Soldier(row,col,self.color,data,True)
                        data.curMana -= 5
                    elif(data.curMana < 15):
                        if(random.randint(0,10) < 6):
                            data.creatures[row][col]=Archer(row,col,self.color,data,True)
                            data.curMana -= 10
                        else:
                            data.creatures[row][col]=Soldier(row,col,self.color,data,True)
                            data.curMana -= 5
                    elif(data.curMana < 20):
                        if(random.randint(0,10) < 5):
                            data.creatures[row][col]=Soldier(row,col,self.color,data,True)
                            data.curMana -= 5
                        else:
                            data.creatures[row][col]=Executor(row,col,self.color,data,True)
                            data.curMana -= 15
                    else:
                        if(random.randint(0,10) < 5):
                            data.creatures[row][col] = Wizard(row,col,self.color,data,True)
                            data.curMana -= 20
                        else:
                            data.creatures[row][col]=Soldier(row,col,self.color,data,True)
                            data.curMana -= 5
                else:
                    for button in data.buttons:
                        if(isinstance(button,EndTurnButton)):
                            button.click(data)
                # recruitment
        elif(isinstance(data.selectedCreature,Creature)):
            if(data.selectedCreature.turns==0):
                data.selectedCreature.movablePlaces = []
                data.selectedCreature.selected = False
                data.selectedCreature = None
            elif(data.selectedCreature.kind=="wizard"): # move this wizard
                if(data.curMana >= 3):
                    enemiesInTwo = self.enemyInTwo(data)
                    if (len(enemiesInTwo) != 0): # make land change spell here
                        enemyPos = random.choice(enemiesInTwo)
                        (row,col) = enemyPos
                        data.squares[row][col] = None
                        data.creatures[row][col] = None
                        data.curMana -= 3
                        data.selectedCreature.turns -= 1
                    elif(random.randint(0,10) < 8):
                        emptySquares = self.getEmptySquares(data)
                        if(len(emptySquares) != 0):
                            (row,col) = random.choice(emptySquares)
                            data.squares[row][col] = Square(row,col)
                            data.curMana -= 3
                            data.selectedCreature.turns -= 1
                        else:
                            self.normalMove(data)
                    else:
                        self.normalMove(data)
                else:
                    self.normalMove(data)
            elif(data.selectedCreature.kind=="executor"):
                if(data.curMana >= 10 and not data.selectedCreature.transformed
                    and data.selectedCreature.turns == 2):
                    enemiesInThree = self.canKillThree(data)
                    if(enemiesInThree != None):
                        (row,col) = enemiesInThree[0]
                        data.selectedCreature.transform()
                        data.curMana -= 10
                        self.normalKillOrMove(data,row,col)
                    else:
                        self.normalKillOrMove(data)
                else:
                    self.normalKillOrMove(data)
            elif(data.selectedCreature.kind=="archer"):
                if(data.curMana >= 10 and not data.selectedCreature.transformed):
                    if(len(enemyCreatures["wizard"])!=0 or len(enemyCreatures["executor"])!=0):
                        enemies = enemyCreatures["wizard"]+enemyCreatures["executor"]
                        for enemy in enemies:
                            if(abs(enemy.row-data.selectedCreature.row)<=6 and
                                abs(enemy.col-data.selectedCreature.col)<=6):
                                data.selectedCreature.transform()
                                data.curMana -= 10
                                self.normalKillOrMove(data,enemy.row,enemy.col)
                                return
                    elif(self.mana >= data.player1.mana+15):
                        pos = random.choice(enemyLand)
                        (row,col) = pos
                        data.selectedCreature.transform()
                        data.curMana -= 10
                        self.normalKillOrMove(data,row,col)
                        return
                self.archerKillOrMove(data)
            elif(data.selectedCreature.kind=="soldier"): # to be edited
                if(data.curMana >= 10 and not data.selectedCreature.transformed):
                    path = self.areTwoSpaceInRow(data)
                    if(path!=None):
                        data.selectedCreature.transform()
                        data.curMana -= 10
                if(data.selectedCreature.transformed):
                    self.normalMove(data)
                else:
                    self.normalKillOrMove(data)
    # following are an alternative way to find the islands, which does not
    # work as well as the current way

    # def isIsland(self,data,row,col):
    #     if(data.squares[row][col].color == self.color):
    #         return False
    #     for checkRow in range(row-1,row+2):
    #         for checkCol in range(col-1,col+2):
    #             if(0<= checkRow <= 9 and 0<= checkCol <= 9 and
    #                 (checkRow != row or checkCol != col) and
    #                 data.squares[checkRow][checkCol] != None and
    #                 data.squares[checkRow][checkCol].color == self.color):
    #                 return False
    #     return True

    # def calculateDistance(self,data,row1,col1,row2,col2):
    #     return max(abs(row1-row2),abs(col1-col2))

    # def toHelpIslandsPresent(self,data,allLands):
    #     answerRow = answerCol = None
    #     for pos in allLands:
    #         (row,col) = pos
    #         if self.isIsland(data,row,col):
    #             (answerRow,answerCol) = (row,col)
    #             break
    #     if(answerRow != None):
    #         lowestDistance = None
    #         nearestRow = nearestCol = None
    #         for pos in allLands:
    #             (row,col) = pos
    #             if(data.squares[row][col].color != self.color):
    #                 break
    #             distance = self.calculateDistance(data,row,col,answerRow,answerCol)
    #             if(distance != 0):
    #                 if(distance == 2):
    #                     (nearestRow,nearestCol) = (row,col)
    #                     break
    #                 elif(lowestDistance == None or distance < lowestDistance):
    #                     lowestDistance = distance
    #                     (nearestRow,nearestCol) = (row,col)
    #     else:
    #         return None
    #     resultRow = nearestRow-1 if nearestRow > answerRow else nearestRow +1
    #     resultCol = nearestCol-1 if nearestCol > answerCol else nearestCol +1
    #     return (resultRow,resultCol)

    def getCreatures(self,data):
        result = {
        "wizard": [],
        "executor": [],
        "archer": [],
        "soldier": []
        }
        enemyResult = {
        "wizard": [],
        "executor": [],
        "archer": [],
        "soldier": []
        }
        availableLand = []
        allLands = []
        enemyLand = []
        for row in range(10):
            for col in range(10):
                creature = data.creatures[row][col]
                land = data.squares[row][col]
                if(land != None and land.landType != "obstacle"):
                    allLands.append((row,col))
                if(land != None and land.color != None and
                        land.color != self.color):
                    enemyLand.append((row,col))
                if(land != None and creature == None and land.color == self.color):
                    availableLand.append((row,col))
                elif (creature != None and creature.color == self.color
                    and (creature.turns != 0 and not creature.summonSickness)):
                    result[creature.kind].append(creature)
                elif (creature != None and creature.color != self.color):
                    enemyResult[creature.kind].append(creature)
        return (result,enemyResult,enemyLand,availableLand,allLands)

    def enemyInTwo(self,data): # wizards only
        result = []
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-2,row+3):
            for checkCol in range(col-2,col+3):
                if (0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    (abs(checkRow-row) == 2 or abs(checkCol-col) == 2)):
                    if (data.squares[checkRow][checkCol] != None and
                        data.creatures[checkRow][checkCol] != None and
                        data.creatures[checkRow][checkCol].color != self.color):
                        result.append((checkRow,checkCol))
        return result

    def getEmptySquares(self,data): #wizards only
        result = []
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-1,row+2):
            for checkCol in range(col-1,col+2):
                if (0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    data.squares[checkRow][checkCol] == None):
                    result.append((checkRow,checkCol))
        return result

    def canKillThree(self,data): # executor only
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-1,row+2):
            for checkCol in range(col-1,col+2):
                if(0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    data.squares[checkRow][checkCol] != None and
                    data.creatures[checkRow][checkCol] != None and
                    data.creatures[checkRow][checkCol].color != self.color):
                    for check2Row in range(checkRow-1,checkRow+2):
                        for check2Col in range(checkCol-1,checkCol+2):
                            if(0<= check2Row <= 9 and 0 <= check2Col <= 9 and
                                data.creatures[check2Row][check2Col]!=None and
                                data.creatures[check2Row][check2Col].color != self.color):
                                return [(checkRow,checkCol),(check2Row,check2Col)]

    def areTwoSpaceInRow(self,data): # soldier only
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-1,row+2):
            for checkCol in range(col-1,col+2):
                if(0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    data.squares[checkRow][checkCol] != None and
                    data.creatures[checkRow][checkCol] == None and
                    data.squares[checkRow][checkCol].color != self.color):
                    for check2Row in range(checkRow-1,checkRow+2):
                        for check2Col in range(checkCol-1,checkCol+2):
                            if(0<= check2Row <= 9 and 0 <= check2Col <= 9 and
                                data.squares[check2Row][check2Col] != None and
                                data.creatures[check2Row][check2Col] == None and
                                data.squares[check2Row][check2Col].color != self.color):
                                    return (checkRow,checkCol)

    def archerKillOrMove(self,data): # archer only
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-2,row+3):
            for checkCol in range(col-2,col+3):
                if(0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    (abs(checkRow-row) == 2 or abs(checkCol-col) == 2)):
                    if(data.creatures[checkRow][checkCol] != None and
                        data.creatures[checkRow][checkCol].color != self.color):
                        data.selectedCreature.move(checkRow,checkCol,data)
                        return
        self.normalMove(data)

    def selectCreature(self,data,creature):
        data.selectedCreature = creature
        creature.selected = True

    def normalKillOrMove(self,data,row=None,col=None):
        # normal move possibly killing
        if(row != None):
            data.selectedCreature.move(row,col,data)
        else:
            result = []
            row = data.selectedCreature.row
            col = data.selectedCreature.col
            for checkRow in range(row-1,row+2):
                for checkCol in range(col-1,col+2):
                    if(0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                        data.creatures[checkRow][checkCol]!=None and
                        data.creatures[checkRow][checkCol].color != self.color):
                        result.append((checkRow,checkCol))
            if(len(result)!=0): # kill first then move
                (row,col) = random.choice(result)
                data.selectedCreature.move(row,col,data)
            else:
                self.normalMove(data)

    def normalMove(self,data): # normal move without killing
        possibilities = []
        row = data.selectedCreature.row
        col = data.selectedCreature.col
        for checkRow in range(row-1,row+2):
            for checkCol in range(col-1,col+2):
                if (0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                    (checkRow != row or checkCol != col)):
                    if(data.squares[checkRow][checkCol] != None and
                        data.squares[checkRow][checkCol].landType != "obstacle"
                        and data.creatures[checkRow][checkCol] == None):
                        possibilities.append((checkRow,checkCol))
        superGoodMoves = []
        goodMoves = []
        badMoves = []
        superBadMoves = []
        normalMoves = []
        if(len(possibilities) != 0):
            for position in possibilities:
                if(self.goodPosition(data,position)):
                    superGoodMoves.append(position)
                elif(self.normalPosition(data,position)):
                    goodMoves.append(position)
                elif(self.isDangerousSquare(data,position)):
                    if(data.selectedCreature.kind == "wizard" or
                            data.selectedCreature.kind == "archer"):
                        superBadMoves.append(position)
                    else:
                        badMoves.append(position)
                elif(self.uselessSquare(data,position)):
                    if(data.selectedCreature.kind == "executor" or
                        data.selectedCreature.kind == "soldier"):
                        superBadMoves.append(position)
                    else:
                        badMoves.append(position)
                else:
                    normalMoves.append(position)
        else:
            data.selectedCreature.turns = 0
            return
        random.shuffle(goodMoves)
        random.shuffle(badMoves)
        random.shuffle(normalMoves)
        random.shuffle(superBadMoves)
        random.shuffle(superGoodMoves)
        possibilities = (superGoodMoves + goodMoves +
                        normalMoves + badMoves + superBadMoves)
        (drow, dcol) = possibilities[0]
        data.selectedCreature.move(drow,dcol,data)

    def uselessSquare(self,data,position):
        (row,col) = position
        if(data.squares[row][col].color == self.color):
            return True
        return False

    def isDangerousSquare(self,data,position): # one included
        (row,col) = position
        for checkRow in range(row-1,row+2):
            for checkCol in range(col-1,col+2):
                if (checkRow != row or checkCol != col):
                    if(0<= checkRow <= 9 and 0 <= checkCol <= 9 and
                        data.creatures[checkRow][checkCol] != None and
                        data.creatures[checkRow][checkCol].color != self.color):
                        return True
        return False

    def goodPosition(self,data,position):
        (row,col) = position
        if(data.squares[row][col].landType != "bridge" and
            data.squares[row][col].color != None and
            data.squares[row][col].color != self.color):
            return True
        return False

    def normalPosition(self,data,position):
        (row,col) = position
        if(data.squares[row][col].landType != "bridge" and
            data.squares[row][col].color == None):
            return True
        return False


from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QScrollArea, QGroupBox, QWidget, QFormLayout, \
    QLabel, QLineEdit, QHBoxLayout, QBoxLayout, QSizePolicy,QStackedWidget ,QVBoxLayout,QGridLayout,QCheckBox,QMessageBox
from PyQt5 import QtCore
from PyQt5.QtGui import QFont
from FileUtils import FileUtil
import re
import sys

class AddReminderWindow(QWidget):

    Update = QtCore.pyqtSignal()
    def __init__(self):
        super().__init__()
        self.title ='Add Reminder'
        self.left = 500
        self.top = 200
        self.width_ = 600
        self.height = 600
        self.NoBoderStyleSheet = ("border :0px;\n")
        self.setStyleSheet("background-color: black;\n" "padding:0px;\n" "spacing:0px;\n")
        self.InitWindow()
        layout = self.InitUI()
        gridLayout = QVBoxLayout()
        gridLayout.addStretch()
        gridLayout.setSpacing(0)
        gridLayout.setContentsMargins(0,0,0,0)
        self.setLayout(layout)

    def mousePressEvent(self, event):
        if event.button() == 1:
            self.isMouseButtonDown = True
            self.oldPos = event.globalPos()

    def mouseReleaseEvent(self, event):
        self.isMouseButtonDown = False

    def mouseMoveEvent(self, event):
        if self.isMouseButtonDown == True:
            delta =  (event.globalPos() - self.oldPos)
            self.move(self.x() + delta.x(), self.y() + delta.y())
            self.oldPos = event.globalPos()

    def AppendReminder(self):
        if self.Title.text()  == '' or self.Date.text()== '' or self.StartTime.text()== '' or self.EndTime.text()=='':
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText('Missing Fields')
            msg.setWindowTitle("Error")
            msg.exec_()
            return
        if not re.search("..-..-....",self.Date.text()):
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText('Enter Data in the format DD-MM-YYYY')
            msg.setWindowTitle("Error")
            msg.exec_()
            return
        if not re.search("..:..",self.StartTime.text()):
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText('Enter time in the format HH:MM')
            msg.setWindowTitle("Error")
            msg.exec_()
            return

        if not re.search("..:..",self.EndTime.text()):
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Critical)
            msg.setText("Error")
            msg.setInformativeText('Enter time in the format HH:MM')
            msg.setWindowTitle("Error")
            msg.exec_()
            return

        FileUtil.saveRemindersToFile(self,self.Title.text(),self.Date.text(),self.StartTime.text(),self.EndTime.text())
        self.Title.clear()
        self.Date.clear()
        self.StartTime.clear()
        self.EndTime.clear()
        self.Update.emit()
        self.close()

    def InitTopBar(self):
        TopBar = QHBoxLayout()
        TopBar.setContentsMargins(0,0,0,0)
        Back = QPushButton('')
        Back.setMinimumHeight(45)
        Back.setMinimumWidth(45)
        Back.setStyleSheet("QPushButton{background-color: black;\n color: black;\n border: 1px solid black;\n border-radius:25px;\n padding:10px;\n image: url(Back.png);\n}"
                            "QPushButton::hover{background-color : #31034a;}\n")
        Back.clicked.connect(lambda: self.close())
        TopBar.addWidget(Back)
        TopBar.addStretch()
        tempBox = QGroupBox()
        tempBox.setContentsMargins(0,0,0,0)
        tempBox.setMaximumHeight(45)
        tempBox.setMaximumWidth(45)
        tempBox.setStyleSheet("background-color: black;\n" "padding:0px;\n"+self.NoBoderStyleSheet)
        tempBox.setLayout(TopBar)
        return tempBox

    def InitUI(self):
        Layout = QVBoxLayout()
        Layout.setContentsMargins(10,10,10,10)
        Layout.setSpacing(5)
        TopBar = self.InitTopBar()

        self.Title = QLineEdit()
        self.Title.setFont(QFont('Open Sans',15))
        self.Title.setStyleSheet("background-color: grey;\n color: White;\n border: 1px solid black;\n border-radius:25px;\n padding:15px;\n\n}")
        self.Title.setMinimumHeight(50)
        self.Title.setMinimumWidth(400)
        self.Title.setPlaceholderText('Enter Title')
        Layout.addWidget(self.Title)

        self.Date = QLineEdit()
        self.Date.setFont(QFont('Open Sans',15))
        self.Date.setStyleSheet("background-color: grey;\n color: White;\n border: 1px solid black;\n border-radius:25px;\n padding:15px;\n\n}")
        self.Date.setMinimumHeight(50)
        self.Date.setMinimumWidth(400)
        self.Date.setPlaceholderText('Enter Date')
        Layout.addWidget(self.Date)

        self.StartTime = QLineEdit()
        self.StartTime.setFont(QFont('Open Sans',15))
        self.StartTime.setStyleSheet("background-color: grey;\n color: White;\n border: 1px solid black;\n border-radius:25px;\n padding:15px;\n\n}")
        self.StartTime.setMinimumHeight(50)
        self.StartTime.setMinimumWidth(400)
        self.StartTime.setPlaceholderText('Enter Start Time')
        Layout.addWidget(self.StartTime)

        self.EndTime = QLineEdit()
        self.EndTime.setFont(QFont('Open Sans',15))
        self.EndTime.setStyleSheet("background-color: grey;\n color: White;\n border: 1px solid black;\n border-radius:25px;\n padding:15px;\n\n}")
        self.EndTime.setMinimumHeight(50)
        self.EndTime.setMinimumWidth(400)
        self.EndTime.setPlaceholderText('Enter End Time')
        Layout.addWidget(self.EndTime)

        addReminder = QPushButton('Add')
        addReminder.setStyleSheet("QPushButton{background-color: #52057b;\n color: White;\n border: 1px solid #52057b;\n border-radius:25px;\n padding:10px;\n }"
                            "QPushButton::hover{background-color : #31034a;}\n")
        addReminder.setMinimumHeight(50)
        addReminder.setMinimumWidth(400)
        addReminder.clicked.connect(self.AppendReminder)
        Layout.addWidget(addReminder)
        tempBox = QGroupBox()

        tempBox.setStyleSheet("background-color: Black;\n" "padding:0px;\n"+self.NoBoderStyleSheet)
        tempBox.setLayout(Layout)

        templayout = QVBoxLayout()
        templayout.setContentsMargins(0, 0, 0, 10)
        templayout.setSpacing(0)
        templayout.addWidget(TopBar)
        templayout.addWidget(tempBox)
        return templayout

    def printTest(self):
        print('test')

    def InitWindow(self):
        self.setWindowTitle(self.title)
        flags = QtCore.Qt.WindowFlags(QtCore.Qt.FramelessWindowHint )
        self.setWindowFlags(flags)


from distutils.log import debug
from time import time
import numpy as np
import threading
from matplotlib import pyplot as plt
import lds_driver
import time
from data_rw import data_send, init_data_rw, data_read, data_read_test

# This one to put global vars
x_stack,y_stack  = [],[]
cur_x_pos, cur_y_pos = [], []
last_angle, truth_angle= 0, 0 # This one used for null shift
cur_vx, cur_vy, cur_angle, mode, error_status = 0, 0, 0, 1, 0
target_v, target_angle, cur_point = 0, 0, 0
delta_angle_manual = 0
record_end = False # Indicating record ends or not

def record_input(cur_state, x_stack, y_stack, spd_x, spd_y, ang, t_interval = 1):
    if (x_stack == []):
        # Handle initial condition
        x_stack.append(0.0)
        y_stack.append(0.0)
    # print("stack::",x_stack[-1], spd_x, spd_y)
    x_new = x_stack[-1] + t_interval * (np.cos(ang/180*np.pi) * spd_y + np.cos((ang+90)/180*np.pi) * spd_x)
    y_new = y_stack[-1] + t_interval * (np.sin(ang/180*np.pi) * spd_y + np.sin((ang+90)/180*np.pi) * spd_x)
    x_stack.append(x_new)
    y_stack.append(y_new)
    spd = np.sqrt(spd_x**2+spd_y**2)
    #print('In ',x_new, y_new, float(spd))
    return

def record_output(x_stack, y_stack, cur_point, ins_spd=100):
    global cur_x_pos, cur_y_pos
    x_direc = x_stack[cur_point] - cur_x_pos[-1]
    y_direc = y_stack[cur_point] - cur_y_pos[-1]
    ins_ang = np.arctan2(y_direc, x_direc)/np.pi*180 # radius, should change to degree
    ins_ang += 360 * (ins_ang<0)
    #print('Out ', x_direc,y_direc,ins_spd, ins_ang)
    global target_v, target_angle
    target_v, target_angle = ins_spd, ins_ang

    #print("the nxt point we gonna go", x_stack[cur_point], y_stack[cur_point])
    if (abs(x_direc)<150 and abs(y_direc)<150):
        return 1

    return 0

def lds_hold(cur_state, ser):
    if cur_state == 0:
        rge = lds_driver.lds_poll(ser)
        if min(rge) < 1:
            return 1
    return 0

def stm32_communication():
    '''
        This code keep talking to stm32, and update info in the global vars
    '''
    global x_stack, y_stack, last_angle, truth_angle
    global cur_vx, cur_vy, cur_angle, mode, error_status, target_v, target_angle, record_end, cur_point
    global ang_out, delta_angle_manual, cur_x_pos, cur_y_pos
    #dev =  init_data_rw()# will be used later in the communication
    ang_out = 0
    # delta_angle_manual = 0
    delta_angle, last_angle, cur_angle = 0, 0, 0
    pre_mode = None
    #cur_vx, cur_vy, cur_angle, mode, error_status = data_read(dev)

    if debug_mode == 2:
        target_angle = cur_angle
    while(1):
        t = time.time()
        # Mode 0: nano control; Mode 1: Remote control; Mode 2: Programming
        #cur_vx, cur_vy, cur_angle, mode, error_status = data_read(dev)
        #cur_vx, cur_vy, cur_angle, mode, error_status = data_read_test(dev,t)
        if (truth_angle == 0):
            truth_angle = cur_angle
            last_angle = cur_angle
        elif (last_angle-cur_angle > 0.2): # handle null shift
            truth_angle = cur_angle
        last_angle = cur_angle
        if (mode == 2):
            if (record_end == 1):
                record_end, cur_point = 0, 0
                x_stack, y_stack = [],[]

            record_input(mode, x_stack, y_stack, cur_vx, cur_vy, truth_angle)
        elif (mode == 0):
            if (pre_mode != mode):
                record_end = 1
                cur_x_pos, cur_y_pos = [], []

            record_input(mode, cur_x_pos, cur_y_pos, cur_vx, cur_vy, truth_angle)

        pre_mode = mode

        if True: #(mode == 0):           #THIS IS CHANGED FOR TESTING 2022.03.28
            delta_angle =  - last_angle + target_angle
            if delta_angle > 180:
                delta_angle = delta_angle - 360
            elif delta_angle < -180:
                delta_angle = delta_angle + 360

            if mode != 0:
                delta_angle = 0

            #The plus 180 work is moved to data_rw, here is only [-180,180] indicating the desired degree
            if debug_mode != 2:
                data = [target_v, delta_angle, mode, error_status]
                if (target_angle == 0):  # Fix the cold start bug
                    continue
                if (mode == 0):
                    #print("data", data)
                    #print("fucking output angle", last_angle, target_angle, delta_angle, mode)
                    ang_out = delta_angle
                    data_send(data, dev)
            elif debug_mode == 2:
                data = [target_v, delta_angle_manual, mode, error_status]
                ang_out = delta_angle_manual
                data_send(data, dev)

def route_decision():
    '''
        This function decide the route based on given info, only works when mode become 0
    '''
    global x_stack, y_stack, last_angle, truth_angle
    global cur_vx, cur_vy, cur_angle, mode, error_status, target_v, target_angle, record_end, cur_point
    global debug_mode

    if debug_mode == 0:  #Onlt init if lds_fix_route is chosen
        ser = lds_driver.lds_driver_init()
    while (1):
        if (record_end == True) and (debug_mode != 2) and (debug_mode != 3):  #output fix route when debug mode is not manual
            if debug_mode == 0:
                if lds_hold(ser) == 1:
                    print('LDS Hold Start')
                    target_v, target_angle = 0, last_angle
                    time.sleep(5)
                    print('LDS Hold End')

            if (record_output(x_stack, y_stack, cur_point)==1):  # To check whether we get the point and move on to the next point
                cur_point += 5

            if (cur_point >= len(x_stack) - 1):
                cur_point = 0

def monitor():
    '''
        This function monitors the running of the system, prints out needed elements for debugging
        Also is for doing manual debug
    '''
    global x_stack, y_stack, last_angle, truth_angle
    global cur_vx, cur_vy, cur_angle, mode, error_status, target_v, target_angle, record_end, cur_point
    global ang_out, debug_mode, delta_angle_manual

    ang_out = 0
    # Mode 0: nano control
    # Mode 1: Remote control
    # Mode 2: Programming
    # Debug mode 0: fix route program
    # Debug mode 1: fix route program without LDS
    # Debug mode 2: manual speed, directly assigned delta angle
    # Debug mode 3: manual speed, absolute target angle

    debug_mode = 2
    debug_duration_time = 10
    manual_target_v = 0
    manual_target_angle = 115
    detla_angle_manual_private = 5
    ##
    is_debug = 'inactive'
    start_time = time.time()
    while(1):
        # if debug_mode == 2:
        #     data = [target_v, delta_angle_manual, mode, error_status]
        #     # ang_out = delta_angle_manual
        #     data_send(data, dev)
        if debug_mode == 2 or debug_mode == 3:
            if True: #time.time() - start_time < debug_duration_time:
                target_v = manual_target_v
                if debug_mode == 2:
                    delta_angle_manual = detla_angle_manual_private
                elif debug_mode == 3:
                    target_angle = manual_target_angle
                is_debug = 'active'
            else:
                target_v = 0
                target_angle = last_angle
                is_debug = 'holding'

        #print('read: ', cur_vx, cur_vy, cur_angle, ' || ', 'send: ', target_v, ang_out, ' || ', 'mode: ', mode, ' || ','debug: ', is_debug)
        #print('running')
        #time.sleep(0.05)
        # v, angle: mm/s and degree, both absolute value, ang_out is [-180, 180]

def test_thread():
    dev =  init_data_rw()
    while 1:
        t = time.time()
        cur_vx, cur_vy, cur_angle, mode, error_status = data_read_test(dev,t)
        #print('running')

def fix_route_main():
    '''
        This code implement the fix route algorithm, forcing the robot to record the message, memorize it,
        then go along this route. When detecting somebody nearby, stop.
    '''
    # Now comes the recording mode
    #t3 = threading.Thread(target = monitor)
    #t1 = threading.Thread(target = stm32_communication)
    #t2 = threading.Thread(target = route_decision)
    t4 = threading.Thread(target = test_thread)
    t4.start()
    #t3.start()
    #t1.start()
    #t2.start()

    while(1):
        pass

# parser = argparse.ArgumentParser()
# parser.add_argument("-mode", default="RUN")
# opt = parser.parse_args()
fix_route_main()


import numpy as np
from numpy import e
from itertools import product

from .learner import Learner
from IPython import embed
from scipy.special import softmax

import gym

class ADP(Learner):
	def __init__(
		self,
		action_space,
		observation_space,
		bins,
		initial_temp=5000,
		gamma=0.99,
		delta=0.1
	):
		super().__init__()
		self.bins = bins
		self.n_actions = action_space.n
		self.n_obs = observation_space.shape[0]
		self.gamma = gamma
		self.delta = delta

		self.mins = observation_space.low
		self.maxes = observation_space.high
		sp_shape = [bins+1 for _ in range(self.n_obs)]
		sp_shape[0] += 1
		sp_shape = tuple(sp_shape)
		self._temp = initial_temp

		self.F = {}
		self.R = {}

		self._terminal_state = tuple([i-1 for i in sp_shape])

		self.V = {}
		self.visits = {}

		self.bounds = np.array([
			np.linspace(self.mins[i], self.maxes[i], bins)
			for i in range(self.n_obs)
		])

		self._statemap = {}

	def upsilon(self, s):
		s = self._convert_to_discrete(s)

		if s in self.visits:
			return np.sqrt(self.visits[s])

		return 0

	def psi(self, s):
		vals = self.get_action_vals(s)

		return self.upsilon(s) * np.abs(np.max(vals) - np.min(vals))

	def confidence(self, s):
		s = self._convert_to_discrete(s)

		if tuple(s) not in self.visits:
			return 0

		return 1 - (1 / (np.sqrt(self.visits[s])))

	def p_sp(self, s, a, sp):
		expected_reward = np.mean(self.R[tuple(s)][a][tuple(sp)])

		total_s_a = 0
		for tmp in self.F[tuple(s)][a]:
			total_s_a += self.F[tuple(s)][a][tmp]

		p_sp = (self.F[tuple(s)][a][tuple(sp)]) / total_s_a

		return p_sp, expected_reward

	def handle_transition(self, s, a, r, sp, done):
		s = self._convert_to_discrete(s)
		sp = self._convert_to_discrete(sp)

		a = int(a)

		if done:
			sp = self._terminal_state

		if tuple(s) in self.visits:
			self.visits[tuple(s)] += 1
		else:
			self.visits[tuple(s)] = 1

		if tuple(s) not in self.F:
			self.F[tuple(s)] = {}
		if a not in self.F[tuple(s)]:
			self.F[tuple(s)][a] = {}
		if tuple(sp) not in self.F[tuple(s)][a]:
			self.F[tuple(s)][a][tuple(sp)] = 0

		self.F[tuple(s)][a][tuple(sp)] += 1

		if tuple(s) not in self.R:
			self.R[tuple(s)] = {}
		if a not in self.R[tuple(s)]:
			self.R[tuple(s)][a] = {}
		if tuple(sp) not in self.R[tuple(s)][a]:
			self.R[tuple(s)][a][tuple(sp)] = []

		self.R[tuple(s)][a][tuple(sp)].append(r)

		if tuple(s) + (a,) in self._statemap:
			self._statemap[tuple(s) + (a,)].add(sp)
		else:
			self._statemap[tuple(s) + (a,)] = set([sp])

		self._temp = max(1, self._temp-1)
		passes = 0
		while self.do_pass() > self.delta:
			passes += 1

	def do_pass(self):
		delta = 0
		for s in self.F:
			if tuple(s) in self.V:
				v = self.V[tuple(s)]
			else:
				v = 0.
			self.V[tuple(s)] = np.max(self.get_action_vals(s))
			delta = np.max([delta, np.abs(v - self.V[tuple(s)])])

		return delta

	def get_action_vals(self, s):
		s = self._convert_to_discrete(s)

		vals = []
		for a in range(self.n_actions):
			total = 0
			if tuple(s) + (a,) in self._statemap:
				for sp in set(self._statemap[tuple(s) + (a,)]):
					p_sp, e_r = self.p_sp(s, a, sp)
					if sp == self._terminal_state:
						v_sp = 0
					else:
						if tuple(sp) in self.V:
							v_sp = self.V[tuple(sp)]
						else:
							v_sp = 0.

					total += p_sp * (e_r + self.gamma * v_sp)

			vals.append(total)

		return np.array(vals)

	def _convert_to_discrete(self, s):
		bounds = self.bounds
		if type(s) is tuple:
			return s

		new_obs = tuple(
			np.searchsorted(bounds[i], s[i])
			for i in range(self.n_obs)
		)

		return new_obs

	def sample_state(self, s, n):
		# Take in a discretized state and return `n` random states in that "bin"
		if not all(type(i) == int for i in s):
			s = self._convert_to_discrete(s)

		left_edge = np.array(s) - 1
		right_edge = np.array(s)
		right_edge[right_edge == self.bins] -= 1

		gen = np.array([
			np.random.uniform(low=self.bounds[idx][l], high=self.bounds[idx][r], size=n)
			for idx, (l, r)
			in enumerate(zip(left_edge, right_edge))
		]).T

		return gen

	def simulate_step(self, s, a, n=1):
		if not all(type(i) == int for i in s):
			s = self._convert_to_discrete(s)

		sp_s = []
		r_s = []
		p_s = []
		for sp in self.F[s][a]:
			p_sp, r = self.p_sp(s, a, sp)
			p_s.append(p_sp)
			r_s.append(r)
			sp_s.append(self.continuize_state(sp))

		ns = np.random.choice(len(sp_s), p=np.array(p_s), size=n)
		ns = np.array([
			sp_s[i]
			for i
			in ns
		])

		return ns

	def continuize_state(self, s):
		# Take a discretized state and return the middle continuous state value
		if not all(type(i) == int for i in s):
			s = self._convert_to_discrete(s)

		left_edge = np.array(s) - 1
		right_edge = np.array(s)

		low = np.array([self.bounds[idx][l] for idx, l in enumerate(left_edge)])
		high = np.array([self.bounds[idx][h] for idx, h in enumerate(right_edge)])

		return (low + high) / 2

	def action_probabilities(self, s, temp=None):
		if temp is None:
			temp = self._temp

		s = self._convert_to_discrete(s)

		qs = self.get_action_vals(s)
		with np.errstate(divide='ignore', under='ignore'):
			ps = softmax(qs / temp)

		return ps

	def exploration_policy(self, s):
		s = self._convert_to_discrete(s)

		ps = self.action_probabilities(s)

		return np.random.choice(np.arange(len(ps)), p=ps)

	def exploitation_policy(self, s):
		s = self._convert_to_discrete(s)

		ps = self.action_probabilities(s, temp=1)

		return np.argmax(ps)

		#return np.random.choice(np.arange(len(ps)), p=ps)


import torch
import torch.nn as nn
import torch.nn.functional as F
import pyro
from pyro.nn import PyroSample, PyroModule
import pyro.distributions as dists

class Lenet5Deterministic(nn.Module):
    def __init__(self):
        super(Lenet5Deterministic, self).__init__()

        self.conv1 = nn.Conv2d(1, 6, 5, stride=1)
        self.conv2 = nn.Conv2d(6, 16, 5, stride=1)
        ## Kernel size changed to 4 to fit data (original Lenet was designed for 32x32x1 imgs)
        self.conv3 = nn.Conv2d(16, 120, 4, stride=1)

        self.fc1 = nn.Linear(120, 84)

        ## One output interpreted as the probability of being class 1
        self.fc2 = nn.Linear(84, 1)

        self.activation = nn.Tanh()

        ## Probability as output
        self.output = nn.Sigmoid()

    def forward(self, x):
        x = F.avg_pool2d(self.activation(self.conv1(x)), 2, stride=2)
        x = F.avg_pool2d(self.activation(self.conv2(x)), 2, stride=2)
        x = self.activation(self.conv3(x))

        x = torch.flatten(x, 1) # flatten all dimensions except the batch dimension

        x = self.activation(self.fc1(x))
        x = self.output(self.fc2(x))

        x = x.squeeze(1)
        return x

class Lenet5Bayesian(PyroModule):
    def __init__(self, prior_scale=2., isFEBayesian=True, prior_scaleFE = 2.):
        super(Lenet5Bayesian, self).__init__()
        prior = dists.Normal(0, prior_scale)
        priorFE = dists.Normal(0, prior_scaleFE)

        ## Make the feature extraction part of the NN bayesian?
        if isFEBayesian:
            self.conv1 = PyroModule[nn.Conv2d](1, 6, 5, stride=1)
            self.conv1.weight = PyroSample(priorFE.expand([6, 1, 5, 5]).to_event(4))
            self.conv1.bias = PyroSample(priorFE.expand([6]).to_event(1))

            self.conv2 = PyroModule[nn.Conv2d](6, 16, 5, stride=1)
            self.conv2.weight = PyroSample(priorFE.expand([16, 6, 5, 5]).to_event(4))
            self.conv2.bias = PyroSample(priorFE.expand([16]).to_event(1))

            self.conv3 = PyroModule[nn.Conv2d](16, 120, 4, stride=1)
            self.conv3.weight = PyroSample(priorFE.expand([120, 16, 4, 4]).to_event(4))
            self.conv3.bias = PyroSample(priorFE.expand([120]).to_event(1))
        else:
            self.conv1 = nn.Conv2d(1, 6, 5, stride=1)
            self.conv2 = nn.Conv2d(6, 16, 5, stride=1)
            ## Kernel size changed to 4 to fit data (original Lenet was designed for 32x32x1 imgs)
            self.conv3 = nn.Conv2d(16, 120, 4, stride=1)

        self.fc1 = PyroModule[nn.Linear](120, 84)
        self.fc1.weight = PyroSample(prior.expand([84, 120]).to_event(2))
        self.fc1.bias = PyroSample(prior.expand([84]).to_event(1))

        self.fc2 = PyroModule[nn.Linear](84, 1)
        self.fc2.weight = PyroSample(prior.expand([1, 84]).to_event(2))
        self.fc2.bias = PyroSample(prior.expand([1]).to_event(1))

        ## Same as before, one value as output interpreted as a probability
        self.output = nn.Sigmoid()
        self.activation = nn.Tanh()

    def forward(self, x, y=None):
        x = F.avg_pool2d(self.activation(self.conv1(x)), 2, stride=2)
        x = F.avg_pool2d(self.activation(self.conv2(x)), 2, stride=2)
        x = self.activation(self.conv3(x))

        x = torch.flatten(x, 1) # flatten all dimensions except the batch dimension

        x = self.activation(self.fc1(x))
        x = self.output(self.fc2(x))
        #x = self.fc2(x)
        f = x.squeeze(1)

        #print(f.shape)
        with pyro.plate("data", f.shape[0]):
            loc = pyro.deterministic("k", f, event_dim=0)
            obs = pyro.sample("obs", dists.Bernoulli(probs=loc), obs=y) #Bernoulli likelihood

        return f


from util import manhattanDistance
from game import Directions
import random, util

from game import Agent

class ReflexAgent(Agent):
  """
    A reflex agent chooses an action at each choice point by examining
    its alternatives via a state evaluation function.

    The code below is provided as a guide.  You are welcome to change
    it in any way you see fit, so long as you don't touch our method
    headers.
  """
  def __init__(self):
    self.lastPositions = []
    self.dc = None

  def getAction(self, gameState):
    """
    getAction chooses among the best options according to the evaluation function.

    getAction takes a GameState and returns some Directions.X for some X in the set {North, South, West, East, Stop}
    ------------------------------------------------------------------------------
    Description of GameState and helper functions:

    A GameState specifies the full game state, including the food, capsules,
    agent configurations and score changes. In this function, the |gameState| argument
    is an object of GameState class. Following are a few of the helper methods that you
    can use to query a GameState object to gather information about the present state
    of Pac-Man, the ghosts and the maze.

    gameState.getLegalActions():
        Returns the legal actions for the agent specified. Returns Pac-Man's legal moves by default.

    gameState.generateSuccessor(agentIndex, action):
        Returns the successor state after the specified agent takes the action.
        Pac-Man is always agent 0.

    gameState.getPacmanState():
        Returns an AgentState object for pacman (in game.py)
        state.pos gives the current position
        state.direction gives the travel vector

    gameState.getGhostStates():
        Returns list of AgentState objects for the ghosts

    gameState.getNumAgents():
        Returns the total number of agents in the game

    The GameState class is defined in pacman.py and you might want to look into that for
    other helper methods, though you don't need to.

    """
    # Collect legal moves and successor states
    legalMoves = gameState.getLegalActions()

    # Choose one of the best actions
    scores = [self.evaluationFunction(gameState, action) for action in legalMoves]
    bestScore = max(scores)
    bestIndices = [index for index in range(len(scores)) if scores[index] == bestScore]
    chosenIndex = random.choice(bestIndices) # Pick randomly among the best

    return legalMoves[chosenIndex]

  def evaluationFunction(self, currentGameState, action):
    """
    The evaluation function takes in the current and proposed successor
    GameStates (pacman.py) and returns a number, where higher numbers are better.

    The code below extracts some useful information from the state, like the
    remaining food (oldFood) and Pacman position after moving (newPos).
    newScaredTimes holds the number of moves that each ghost will remain
    scared because of Pacman having eaten a power pellet.
    """
    # Useful information you can extract from a GameState (pacman.py)
    successorGameState = currentGameState.generatePacmanSuccessor(action)
    newPos = successorGameState.getPacmanPosition()
    oldFood = currentGameState.getFood()
    newGhostStates = successorGameState.getGhostStates()
    newScaredTimes = [ghostState.scaredTimer for ghostState in newGhostStates]

    return successorGameState.getScore()

def scoreEvaluationFunction(currentGameState):
  """
    This default evaluation function just returns the score of the state.
    The score is the same one displayed in the Pacman GUI.

    This evaluation function is meant for use with adversarial search agents
    (not reflex agents).
  """
  return currentGameState.getScore()

class MultiAgentSearchAgent(Agent):
  """
    This class provides some common elements to all of your
    multi-agent searchers.  Any methods defined here will be available
    to the MinimaxPacmanAgent, AlphaBetaPacmanAgent & ExpectimaxPacmanAgent.

    You *do not* need to make any changes here, but you can if you want to
    add functionality to all your adversarial search agents.  Please do not
    remove anything, however.

    Note: this is an abstract class: one that should not be instantiated.  It's
    only partially specified, and designed to be extended.  Agent (game.py)
    is another abstract class.
  """

  def __init__(self, evalFn = 'scoreEvaluationFunction', depth = '2'):
    self.index = 0 # Pacman is always agent index 0
    self.evaluationFunction = util.lookup(evalFn, globals())
    self.depth = int(depth)

class MinimaxAgent(MultiAgentSearchAgent):
  """
    Your minimax agent (problem 1)

    The auto grader will check the running time of your algorithm. Friendly reminder: passing the auto grader
    does not necessarily mean that your algorithm is correct.
  """

  def getAction(self, gameState):
    """
      Returns the minimax action from the current gameState using self.depth
      and self.evaluationFunction. Terminal states can be found by one of the following:
      pacman won, pacman lost or there are no legal moves.

      Here are some method calls that might be useful when implementing minimax.

      gameState.getLegalActions(agentIndex):
        Returns a list of legal actions for an agent
        agentIndex=0 means Pacman, ghosts are >= 1

      Directions.STOP:
        The stop direction, which is always legal

      gameState.generateSuccessor(agentIndex, action):
        Returns the successor game state after an agent takes an action

      gameState.getNumAgents():
        Returns the total number of agents in the game

      gameState.isWin():
        Returns True if it's a winning state

      gameState.isLose():
        Returns True if it's a losing state

      self.depth:
        The depth to which search should continue

      It is recommended you have separate functions: value(), max_value(), and min_value() as in the slides
      and call these functions here to make your code understandable.
    """

    # BEGIN_YOUR_CODE (around 35 lines of code expected)
    v, move = self.value(gameState, self.depth)
    return move
    # END_YOUR_CODE

  def isTerminal(self, s):
    return s.isWin() or s.isLose() or len(s.getLegalActions()) == 0

  def value(self, s, d, agentIndex=0):
    if d == 0 or self.isTerminal(s):
      return self.evaluationFunction(s), None
    elif agentIndex == 0:
      return self.maxValue(s, d)
    else:
      return self.minValue(s, d, agentIndex)

  def maxValue(self, s, d):
    v = float('-inf')
    bestMove = None
    legalMoves = s.getLegalActions()
    nextAgent = 1 % s.getNumAgents()
    for move in legalMoves:
      if move == Directions.STOP:
        continue
      succVal = self.value(s.generatePacmanSuccessor(move),d-1 if nextAgent == 0 else d,1)[0]
      if succVal > v:
        v = succVal
        bestMove = move
    return v, bestMove

  def minValue(self, s, d, agentIndex):
    v = float('inf')
    bestMove = None
    legalMoves = s.getLegalActions(agentIndex)
    nextAgent = (agentIndex + 1) % s.getNumAgents()
    for move in legalMoves:
      succVal = self.value(s.generateSuccessor(agentIndex, move),d-1 if nextAgent == 0 else d,nextAgent)[0]
      if succVal < v:
        v = succVal
        bestMove = move
    return v, bestMove

class AlphaBetaAgent(MultiAgentSearchAgent):
  """
    Your minimax agent with alpha-beta pruning (problem 2)

    The auto grader will check the running time of your algorithm. Friendly reminder: passing the auto grader
    does not necessarily mean your algorithm is correct.
  """

  def getAction(self, gameState):
    """
      Returns the minimax action using self.depth and self.evaluationFunction

      The same methods used in MinimaxAgent should also be useful here

      It is recommended you have separate functions: value(), max_value(), and min_value() as in the slides
      and call these functions here to make the code clear
    """

    # BEGIN_YOUR_CODE (around 45 lines of code expected)
    v, move = self.value(gameState, self.depth)
    return move
    # END_YOUR_CODE

  def isTerminal(self, s):
    return s.isWin() or s.isLose() or len(s.getLegalActions()) == 0

  def value(self, s, d, lowerBound=float('-inf'), upperBound=float('inf'), agentIndex=0):
    if d == 0 or self.isTerminal(s):
      return self.evaluationFunction(s), None
    elif agentIndex == 0:
      return self.maxValue(s, d, lowerBound, upperBound)
    else:
      return self.minValue(s, d, lowerBound, upperBound, agentIndex)

  def maxValue(self, s, d, lowerBound, upperBound):
    bestMove = None
    legalMoves = s.getLegalActions()
    nextAgent = 1 % s.getNumAgents()
    for move in legalMoves:
      if move == Directions.STOP:
        continue
      succVal = self.value(s.generatePacmanSuccessor(move),d-1 if nextAgent == 0 else d,lowerBound,upperBound,nextAgent)[0]
      if succVal > lowerBound:
        lowerBound = succVal
        bestMove = move
      if lowerBound >= upperBound:
        return upperBound, None
    return lowerBound, bestMove

  def minValue(self, s, d, lowerBound, upperBound, agentIndex):
    bestMove = None
    legalMoves = s.getLegalActions(agentIndex)
    nextAgent = (agentIndex + 1) % s.getNumAgents()
    for move in legalMoves:
      succVal = self.value(s.generateSuccessor(agentIndex, move),d-1 if nextAgent == 0 else d,lowerBound,upperBound,nextAgent)[0]
      if succVal < upperBound:
        upperBound = succVal
        bestMove = move
      if upperBound <= lowerBound:
        return lowerBound, None
    return upperBound, bestMove

class ExpectimaxAgent(MultiAgentSearchAgent):
  """
    Your expectimax agent (problem 3)

    The auto grader will check the running time of your algorithm. Friendly reminder: passing the auto grader
    does not necessarily mean your algorithm is correct.
  """

  def getAction(self, gameState):
    """
      Returns the expectimax action using self.depth and self.evaluationFunction

      All ghosts should be modeled as choosing uniformly at random from their
      legal moves.

      The same methods used in MinimaxAgent should also be useful here

      It is recommended you have separate functions: value(), max_value(), and expect_value() as in the slides
      and call these functions here to make the code clear
    """

    # BEGIN_YOUR_CODE (around 35 lines of code expected)
    v, move = self.value(gameState, self.depth)
    return move
    # END_YOUR_CODE

  def isTerminal(self, s):
    return s.isWin() or s.isLose() or len(s.getLegalActions()) == 0

  def value(self, s, d, agentIndex=0):
    if d == 0 or self.isTerminal(s):
      return self.evaluationFunction(s), None
    elif agentIndex == 0:
      return self.maxValue(s, d)
    else:
      return self.expValue(s, d, agentIndex)

  def maxValue(self, s, d):
    v = float('-inf')
    bestMove = None
    legalMoves = s.getLegalActions()
    nextAgent = 1 % s.getNumAgents()
    for move in legalMoves:
      if move == Directions.STOP:
        continue
      succVal = self.value(s.generatePacmanSuccessor(move),d-1 if nextAgent == 0 else d,1)[0]
      if succVal > v:
        v = succVal
        bestMove = move
    return v, bestMove

  def expValue(self, s, d, agentIndex):
    v = 0.0
    legalMoves = s.getLegalActions(agentIndex)
    nextAgent = (agentIndex + 1) % s.getNumAgents()
    for move in legalMoves:
      v += self.value(s.generateSuccessor(agentIndex, move),d-1 if nextAgent == 0 else d,nextAgent)[0]
    return v / len(legalMoves), random.choice(legalMoves)

import search
def betterEvaluationFunction(currentGameState):
  """
    Your extreme ghost-hunting, pellet-nabbing, food-gobbling, unstoppable
    evaluation function (problem 4).

    DESCRIPTION:
      The evaluation function calculates a score which is a linear combination of the
      features with different weights. The score is initialized with the current game
      state score.
      Intercept:
        current game state score
      Features:
        distance to nearest food
        distance to nearest ghost
        number of capsules left
        ghost scared time
      Since the priority goal is to let pacman win, the first 2 features have higher
      weights. The second goal is to get higher score (by eating capsules and ghosts),
      the last 2 features have smaller weights.

      Distance to nearest food:
        The closer to the food, the better. The distance is calculated
        using UCS search and use a simple weight / distance function to
        reflect the property that the closer to food, the higher the score should be.
      Distance to nearest ghost:
        The pacman dies if the it collides with the ghost. The farther the pacman
        is to the ghosts, the better. The distance to the nearest ghost is calculated
        by choosing the minimum of the manhattanDistance between the pacman and all
        the ghosts, since it is the most dangerous agent. A special check is applied
        that if the distance gets close to less than 3, there will be a large penalty
        to the score.
      Number of capsules left:
        A small penalty is applied if there are more capsules left. This is to
        encourage the pacman to eat capsules to get some time of safety.
      Ghost scared time:
        If the ghost is scared, the closer the pacman is to the ghost, the better chance
        the ghost will be eaten. So there is a relatively high positive weight associated
        with the scared time. However, if the scared time is about to expire, it is better
        for the pacman to start avoiding the ghost. So a special check for the scared time
        is applied.
  """
  # BEGIN_YOUR_CODE (around 50 lines of code expected)
  if (currentGameState.isWin()):
    return 9999
  elif (currentGameState.isLose()):
    return -9999
  # Initialize with current game state score
  score = currentGameState.getScore()
  numGhost = currentGameState.getNumAgents() - 1
  problemFood = search.createPacmanFoodSearchProblem(currentGameState)
  algorithm = search.UniformCostSearch()
  algorithm.solve(problemFood)
  # Find the distance to nearest food using UCS.
  distanceToFood = algorithm.totalCost
  numCap = len(currentGameState.getCapsules())
  # Small penalty to number of capsules left to encourage eating it.
  score -= numCap * 15.0
  if distanceToFood != None:
    score += 10.0 / distanceToFood
  distanceToGhost = 999999
  bonus = False
  if numGhost > 0:
    for i in range(numGhost):
      ghostState = currentGameState.getGhostState(i + 1)
      distance = util.manhattanDistance(ghostState.getPosition(), currentGameState.getPacmanPosition())
      # Ghost is scared!
      if ghostState.scaredTimer > 0:
        if not bonus:
          bonus = True
          # If the ghost is scared, it is better to get close to the ghost and try to eat it.
          if (ghostState.scaredTimer > 3 and distance < ghostState.scaredTimer):
            score += 20.0 / distance * 5.0
          elif (ghostState.scaredTimer <= 3):
            # The ghost is about to get back to normal, start avoiding
            score -= 20.0 * 5.0 / ghostState.scaredTimer
          else:
            # It is not possible to eat the within scared time since it is too far.
            continue
      else:
        # Find the distance to the nearest ghost
        if (distance < distanceToGhost):
          distanceToGhost = distance
    if (distanceToGhost != 999999):
      # About to be eaten, large penalty.
      if (distanceToGhost <= 1):
        score -= 99999
      # Large penalty to very close ghosts, the closer, the larger the penalty
      elif (distanceToGhost <= 3):
        score -= 100 * 10.0 / distanceToGhost
      else:
        score -= 15.0 / distanceToGhost
  return score
  # END_YOUR_CODE

# Abbreviation
better = betterEvaluationFunction


# This class is used for visualizing the game-states of the Tic Tac Toe game.
# A GameBoard object will be initialized in the constructor of the TicTacToe
# class.
import os
window_length = os.get_terminal_size().columns

class GameBoard:
    # constructor
    def __init__(self):
        self.board = self.createBoard()
        self.move_positions = self.getMovePositions()
        self.x_pos = []
        self.o_pos = []
        self.fillEmpty()

    # creates the 2D board
    def createBoard(self):
        board = []
        for row in range(3):
            for col in range(3):
                board_row = '     |     |     '
                board.append([value for value in board_row])

            if(row != 2):
                board_row ="-----------------"
                board.append([value for value in board_row])
        return board

    # returns a list of tuples of the coordinates on the board that pertain
    # to the 9 game positions
    def getMovePositions(self):
        move_positions = []
        rows = [1,5,9]
        cols = [2,8,14]
        for row in rows:
            for col in cols:
                move_positions.append((row, col))
        return move_positions

    # fills the board's 9 positions with values 0-8
    def fillEmpty(self):
        count = 0
        for pos in self.move_positions:
            self.board[pos[0]][pos[1]] = str(count)
            count += 1

    # returns true if the move is possible to make, boundary error checks
    def checkMove(self, pos):
        if(not (pos <= 8 and pos >= 0) or (pos in (self.x_pos + self.o_pos))):
            print()
            print('You entered an invalid move, try again.'.center(window_length))
            print()
            return False
        return True

    # updates the 2D array to include the player's new move
    def playMove(self, pos, symbol):
        # update the board with the new move
        self.board[self.move_positions[pos][0]][self.move_positions[pos][1]] = symbol
        # track the position (0-8) that has now been taken for each player
        self.x_pos.append(pos) if symbol == 'X' else self.o_pos.append(pos)

    # updates the 2D array to undo the move at the given position
    def undoMove(self, pos, symbol):
        # update the board with undoing the move
        self.board[self.move_positions[pos][0]][self.move_positions[pos][1]] = str(pos)
        self.x_pos.remove(pos) if symbol == 'X' else self.o_pos.remove(pos)

    # returns true if there are moves left, returns false otherwise
    def isMovesLeft(self):
        return True if (9 - (len(self.x_pos) + len(self.o_pos)) > 0) else False

    # print the state of the board to the console
    def printBoard(self):
        print('====================='.center(window_length))
        for row in self.board:
            rowStr = '||'+''.join(row) + '||'
            print(rowStr.center(window_length))
        print('====================='.center(window_length))