from src.game_constants import RobotType, Direction, Team, TileState
from src.game_state import GameState, GameInfo
from src.player import Player
from src.map import TileInfo, RobotInfo
from collections import deque, OrderedDict
import random
import time

def coord_distance(coord1, coord2):
    return max(abs(coord1[0] - coord2[0]), abs(coord1[1] - coord2[1]))

def get_coords(tile):
    return (tile.row, tile.col)

class BotPlayer(Player):
    """
    Players will write a child class that implements (notably the play_turn method)
    """

    def __init__(self, team: Team):
        self.team = team
        self.explorer_goal = {}
        self.miner_goal = {}
        self.miner_charging = {}
        self.terraformer_charging = {}
        self.width = 0
        self.height = 0
        self.map = None
        self.game_state = None
        self.begin_time = 0
        self.print_warnings = False
        self.debug = False
        self.print_time = self.debug
        self.reachable_by_terraformers = None
        self.last_secure_mines_time = 0
        self.connectivity = None
        self.fog_map = None
        self.total_fog_count = 0
        return

    def compute_connectivity(self) -> int:
        self.connectivity = [[None for _ in range(self.width)] for _ in range(self.height)]
        num_blocks = 0
        for row in range(self.height):
            for col in range(self.width):
                if self.map[row][col] is not None:
                    if self.connectivity[row][col] is None:
                        num_blocks += 1
                        queue = deque([(row, col)])
                        self.connectivity[row][col] = num_blocks
                        while (queue):
                            r, c = queue.popleft()
                            for dir in Direction:
                                newRow, newCol = r + dir.value[0], c + dir.value[1]
                                if newRow < 0 or newRow >= self.height or newCol < 0 or newCol >= self.width:
                                    continue
                                if self.map[newRow][newCol] is None:
                                    continue
                                if self.connectivity[newRow][newCol] is not None:
                                    continue
                                self.connectivity[newRow][newCol] = num_blocks
                                queue.append((newRow, newCol))
        return num_blocks

    def compute_reachable_by_terraformers(self, my_terraformers):
        self.reachable_by_terraformers = [[None for _ in range(self.width)] for _ in range(self.height)]
        queue = deque([(terraformer, terraformer.row, terraformer.col) for terraformer in my_terraformers])
        for terraformer in my_terraformers:
            self.reachable_by_terraformers[terraformer.row][terraformer.col] = None
        while (queue):
            terraformer, queueRow, queueCol = queue.popleft()
            for newDir in Direction:
                newRow, newCol = queueRow + newDir.value[0], queueCol + newDir.value[1]
                if newRow < 0 or newRow >= self.height or newCol < 0 or newCol >= self.width:
                    continue
                tileState = self.map[newRow][newCol]
                if tileState is None:
                    continue
                # Check if its legal
                if (tileState.state == TileState.ILLEGAL or tileState.state == TileState.IMPASSABLE):
                    continue
                if (tileState.robot is not None):
                    continue
                if not self.reachable_by_terraformers[newRow][newCol]:
                    self.reachable_by_terraformers[newRow][newCol] = terraformer
                    queue.append((terraformer, newRow, newCol))

    def optimal_terraform(self, startRow: int, startCol: int, exclusions, checkCollisions=True) -> tuple[Direction,tuple[int,int]]:
        visited = set()
        queue = deque([(None, startRow, startCol, 0)])
        while (queue):
            dir, queueRow, queueCol, moves = queue.popleft()
            if (self.map[queueRow][queueCol] is not None and
                    (not checkCollisions or self.map[queueRow][queueCol].robot is None) and
                    self.map[queueRow][queueCol].state == TileState.TERRAFORMABLE and
                    self.map[queueRow][queueCol].terraform <= 0 and
                    (queueRow, queueCol) not in exclusions):
                return (dir, (queueRow, queueCol))
            for newDir in Direction:
                newRow, newCol = queueRow + \
                    newDir.value[0], queueCol + newDir.value[1]
                if newRow < 0 or newRow >= self.height or newCol < 0 or newCol >= self.width:
                    continue
                tileState = self.map[newRow][newCol]
                if tileState is None:
                    continue
                # Check if its legal
                if (tileState.state == TileState.ILLEGAL or tileState.state == TileState.IMPASSABLE):
                    continue
                if (checkCollisions and tileState.robot is not None):
                    continue
                if ((newRow, newCol) not in visited):
                    if dir == None:
                        queue.append((newDir, newRow, newCol, moves+1))
                    else:
                        queue.append((dir, newRow, newCol, moves+1))
                    visited.add((newRow, newCol))

        # If we reached this point, a path to the co-ordinates isn't possible
        return (None, (-1, -1))

    def optimal_explore(self, startRow: int, startCol: int, exclusions, checkCollisions=True):
        visited = set()
        queue = deque([(None, startRow, startCol, 0)])
        result = (None, (-1, -1), -1)
        while (queue):
            dir, queueRow, queueCol, moves = queue.popleft()
            if result[0] is not None and moves > result[2] + 2:  # only explore tiles at most 2 moves farther than the best
                break
            if (self.fog_map[queueRow][queueCol] > 0 and (queueRow, queueCol) not in exclusions):
                if result[0] is None or (self.fog_map[queueRow][queueCol] - self.fog_map[result[1][0]][result[1][1]]
                        >= moves - result[2]):
                    result = (dir, (queueRow, queueCol), moves)
            for newDir in Direction:
                newRow, newCol = queueRow + newDir.value[0], queueCol + newDir.value[1]
                if newRow < 0 or newRow >= self.height or newCol < 0 or newCol >= self.width:
                    continue
                tileState = self.map[newRow][newCol]
                if tileState is None:
                    continue
                # Check if its legal
                if (tileState.state == TileState.ILLEGAL or tileState.state == TileState.IMPASSABLE):
                    continue
                if (checkCollisions and tileState.robot is not None):
                    continue
                if ((newRow, newCol) not in visited):
                    if dir == None:
                        queue.append((newDir, newRow, newCol, moves + 1))
                    else:
                        queue.append((dir, newRow, newCol, moves + 1))
                    visited.add((newRow, newCol))
        return result

    def closest_ally_tile_to_spawn(self, startRow: int, startCol: int, ally_tile_set):
        visited = set()
        queue = deque([(startRow, startCol)])
        while (queue):
            queueRow, queueCol = queue.popleft()
            if ((queueRow, queueCol) in ally_tile_set):
                return (queueRow, queueCol)
            for newDir in Direction:
                newRow, newCol = queueRow + newDir.value[0], queueCol + newDir.value[1]
                if newRow < 0 or newRow >= self.height or newCol < 0 or newCol >= self.width:
                    continue
                tileState = self.map[newRow][newCol]
                if tileState is None:
                    continue
                # Check if its legal
                if (tileState.state == TileState.ILLEGAL or tileState.state == TileState.IMPASSABLE):
                    continue
                if ((newRow, newCol) not in visited):
                    queue.append((newRow, newCol))
                    visited.add((newRow, newCol))

        # If we reached this point, a path to the co-ordinates isn't possible
        return (None, None)

    def try_to_bomb_opponent_mining(self, robot):
        for dir in Direction:  # try to bomb opponent miner in one step if it's mining
            row = robot.row + dir.value[0]
            col = robot.col + dir.value[1]
            if row < 0 or row >= self.height or col < 0 or col >= self.width:
                continue
            if self.map[row][col] is None:  # fogged
                continue
            if self.map[row][col].robot is None or self.map[row][col].robot.team == self.team:
                continue
            if self.map[row][col].state != TileState.MINING:
                continue
            if self.map[row][col].robot.type != RobotType.MINER:
                continue
            if self.map[row][col].robot.battery < 20:  # they cannot mine without battery
                continue
            return dir
        return None

    def count_mine_terraform(self, row, col) -> int:
        terraform_count = 0
        for dir in Direction:
            r = row + dir.value[0]
            c = col + dir.value[1]
            if r < 0 or r >= self.height or c < 0 or c >= self.width:
                continue
            if self.map[r][c] is None:
                continue
            if self.map[r][c].terraform > 0:
                terraform_count += 1
            elif self.map[r][c].terraform < 0:
                terraform_count -= 1
        return terraform_count

    def count_mine_insecure(self, row, col) -> int:
        insecure_count = 0
        for dir in Direction:
            r = row + dir.value[0]
            c = col + dir.value[1]
            if r < 0 or r >= self.height or c < 0 or c >= self.width:
                continue
            if self.map[r][c] is None:
                continue
            if self.map[r][c].state == TileState.TERRAFORMABLE and self.map[r][c].terraform <= 0:
                insecure_count += (-self.map[r][c].terraform) + 1
        return insecure_count

    def count_fog(self, row, col) -> int:
        fogged_count = 0
        for dir in Direction:
            r = row + dir.value[0]
            c = col + dir.value[1]
            if r < 0 or r >= self.height or c < 0 or c >= self.width:
                continue
            if self.map[r][c] is None:
                fogged_count += 1
        return fogged_count

    def init_fog_map(self):
        self.fog_map = [[None for _ in range(self.width)] for _ in range(self.height)]
        for row in range(self.height):
            for col in range(self.width):
                self.fog_map[row][col] = self.count_fog(row, col)

    def move_robot(self, robot, dir) -> bool:
        rname = robot.name
        if self.game_state.can_move_robot(rname, dir):
            new_row = robot.row + dir.value[0]
            new_col = robot.col + dir.value[1]
            if self.map[new_row][new_col] is not None and self.map[new_row][new_col].robot is not None:
                # collision
                if self.map[new_row][new_col].robot.team == self.team:
                    if self.print_warnings:
                        print("Collision with ourselves:", robot, "to", new_row, new_col, "with",
                              self.map[new_row][new_col].robot)
                self.map[new_row][new_col].robot = None
                self.map[robot.row][robot.col].robot = None
            else:
                # move
                if self.map[new_row][new_col] is not None:
                    self.map[new_row][new_col].robot = self.map[robot.row][robot.col].robot
                self.map[robot.row][robot.col].robot = None
            robot.row = new_row
            robot.col = new_col
            robot.moved = True
            self.game_state.move_robot(rname, dir)
            return True
        return False

    def play_turn(self, game_state: GameState) -> None:
        if self.print_time:
            self.begin_time = time.time()
            print("Time left at the beginning:", game_state.get_time_left())
        if game_state.get_time_left() < 5:
            # no more than 5 seconds left, what shall we do?
            return
        # get info
        ginfo = game_state.get_info()

        # get turn/team info
        self.width, self.height = len(ginfo.map), len(ginfo.map[0])
        width, height = self.width, self.height
        self.map = ginfo.map
        self.game_state = game_state

        # print info about the game
        if self.debug:
            print(f"Turn {ginfo.turn}, team {ginfo.team}")
        # print("Map height", height)
        # print("Map width", width)

        # find un-occupied ally tile
        ally_tiles = []
        my_mines = []
        opponent_mines = []
        insecure_mines = OrderedDict()
        my_robots = []
        opponent_robots = []
        fogged_tiles = []
        # edge_tiles = {}
        # for i in range(9):
        #     edge_tiles[i] = []
        self.total_fog_count = 0
        self.init_fog_map()
        for row in range(height):
            for col in range(width):
                # get the tile at (row, col)
                tile = self.map[row][col]
                # skip fogged tiles
                if tile is not None:  # ignore fogged tiles
                    if tile.robot is not None:
                        if tile.robot.team == self.team: # my robot
                            my_robots += [tile.robot]
                            tile.robot.moved = False
                            tile.robot.acted = False
                        else:
                            opponent_robots += [tile.robot]
                    if tile.state == TileState.MINING:
                        terraform_count = 0
                        for dir in Direction:
                            r = row + dir.value[0]
                            c = col + dir.value[1]
                            if r < 0 or r >= height or c < 0 or c >= width:
                                continue
                            if self.map[r][c] is None:
                                continue
                            if self.map[r][c].terraform > 0:
                                terraform_count += 1
                            elif self.map[r][c].terraform < 0:
                                terraform_count -= 1
                        if terraform_count >= 0:
                            my_mines += [(tile.row, tile.col)]
                        else:
                            opponent_mines += [(tile.row, tile.col)]
                    # if tile.state in [TileState.TERRAFORMABLE, TileState.MINING]:
                    #     fogged_count = self.fog_map[row][col]
                    #     if fogged_count > 0:
                    #         edge_tiles[fogged_count] += [tile]
                else:
                    fogged_tiles += [(row, col)]
                    self.total_fog_count += 1

        my_explorers = []
        my_bombs = []
        my_miners = []
        my_terraformers = []
        for robot in my_robots:
            if robot.type == RobotType.EXPLORER:
                if robot.battery < 10:
                    my_bombs += [robot]
                else:
                    my_explorers += [robot]
            elif robot.type == RobotType.MINER:
                my_miners += [robot]
            elif robot.type == RobotType.TERRAFORMER:
                my_terraformers += [robot]

        my_explorer_names = set([robot.name for robot in my_explorers])
        dead_explorer_names = []
        for explorer_name, goal in self.explorer_goal.items():
            if explorer_name not in my_explorer_names:
                dead_explorer_names.append(explorer_name)
        for explorer_name in dead_explorer_names:
            self.explorer_goal.pop(explorer_name)

        if self.debug:
            print(f"My metal {game_state.get_metal()}")
        if self.print_time:
            print("Time after preprocessing:", time.time() - self.begin_time)

        kDistanceToTreatAsReachedGoal = 1
        # move explorers
        if len(fogged_tiles) > 0 and len(my_explorers) > 0:
            exclusions = set()
            for explorer in my_explorers:
                moved = False
                rname = explorer.name
                bomb = self.try_to_bomb_opponent_mining(explorer)
                if bomb is not None:
                    if self.fog_map[explorer.row][explorer.col] > 0 and game_state.can_robot_action(rname):
                        game_state.robot_action(rname)  # last explore before sacrificing
                    if self.move_robot(explorer, bomb):
                        continue
                    else:
                        if self.print_warnings:
                            print("Failed to bomb using explorer!")
                        continue
                if rname in self.explorer_goal.keys():
                    # try to approach goal
                    goal = self.explorer_goal[rname]
                    for goal_dir in Direction:
                        row = goal[0] + goal_dir.value[0]
                        col = goal[1] + goal_dir.value[1]
                        if row < 0 or row >= height or col < 0 or col >= width:
                            continue
                        if self.map[row][col] is None:  # fogged
                            continue
                        path = game_state.optimal_path(explorer.row, explorer.col, row, col, checkCollisions=True)
                        if path[0] is None:
                            continue
                        if self.move_robot(explorer, path[0]):
                            moved = True
                            if coord_distance((explorer.row, explorer.col), goal) <= kDistanceToTreatAsReachedGoal:
                                self.explorer_goal.pop(rname)
                            break
                temp_goal = None
                temp_fog = self.fog_map[explorer.row][explorer.col]
                if not explorer.moved:
                    # move anywhere
                    path = self.optimal_explore(explorer.row, explorer.col, exclusions)
                    if path[0] is not None:
                        if not (temp_fog == 0 or self.fog_map[path[1][0]][path[1][1]] - temp_fog >= path[2]):
                            if not explorer.acted and game_state.can_robot_action(rname):
                                game_state.robot_action(rname)
                                explorer.acted = True
                        if self.move_robot(explorer, path[0]):
                            exclusions.add(path[1])
                            temp_goal = path[1]
                            # also exclude nearby tiles?
                            # for dir in Direction:
                            #     exclusions.add((path[1][0] + dir.value[0], path[1][1] + dir.value[1]))
                    if not explorer.moved:
                        if self.print_warnings:
                            print("Explorer bot not moved", explorer, ", path=", path)
                fogged_count = self.fog_map[explorer.row][explorer.col]
                # explore
                if fogged_count > 0 and not (temp_goal is not None and
                                             (explorer.row != temp_goal[0] or explorer.col != temp_goal[1])):
                    # if there is a better position, wait until the next turn to explore
                    if not explorer.acted and game_state.can_robot_action(rname):
                        game_state.robot_action(rname)

        my_miner_names = set([robot.name for robot in my_miners])
        miner_names_to_pop = []
        for miner_name, _ in self.miner_goal.items():
            if miner_name not in my_miner_names:
                miner_names_to_pop.append(miner_name)
        for miner_name in miner_names_to_pop:
            self.miner_goal.pop(miner_name, None)
        miner_names_to_pop = []
        for miner_name, _ in self.miner_charging.items():
            if miner_name not in my_miner_names:
                miner_names_to_pop.append(miner_name)
        for miner_name in miner_names_to_pop:
            self.miner_charging.pop(miner_name, None)
        mined_mines = set()
        remaining_mines = set()
        # print("Miner goals:", self.miner_goal)
        # print("Miner charging states:", self.miner_charging)
        # print("My miners:", my_miners)
        # print("My mines:", my_mines)
        for miner in my_miners:
            rname = miner.name
            if rname in self.miner_goal.keys():
                mined_mines.add(self.miner_goal[rname])
        for mine in my_mines:
            if mine not in mined_mines:
                remaining_mines.add(mine)
        # print(mined_mines, remaining_mines)
        if self.print_time:
            print("Time after moving explorers:", time.time() - self.begin_time)

        # move miners
        for miner in my_miners:
            rname = miner.name
            bomb = False
            for dir in Direction:  # try to bomb opponent miner in one step if it's mining our mine
                row = miner.row + dir.value[0]
                col = miner.col + dir.value[1]
                if row < 0 or row >= height or col < 0 or col >= width:
                    continue
                if self.map[row][col] is None:  # fogged
                    continue
                if self.map[row][col].robot is None or self.map[row][col].robot.team == self.team:
                    continue
                if self.map[row][col].state != TileState.MINING:
                    continue
                bomb = False
                if rname in self.miner_goal.keys() and self.miner_goal[rname] == (row, col):
                    # bomb whatever robot because this mine belongs to this robot
                    bomb = True
                if self.map[row][col].robot.type == RobotType.MINER:
                    # bomb whatever mine because opponent is mining
                    bomb = True
                if bomb:  # sacrificed
                    if self.move_robot(miner, dir):
                        self.miner_goal.pop(rname, None)
                        self.miner_charging.pop(rname, None)
                        if (row, col) in mined_mines:
                            mined_mines.remove((row, col))
                            remaining_mines.add((row, col))
                        break
            if bomb:
                continue
            if miner.battery < 20:
                self.miner_charging[rname] = True
            if miner.battery >= 100:
                self.miner_charging[rname] = False
            if self.miner_charging[rname]:
                # print("Miner charging", miner, self.miner_goal[rname])
                if self.map[miner.row][miner.col].terraform <= 0:  # not charging
                    path = game_state.robot_to_base(rname, checkCollisions=True)
                    if path[0] is not None and self.move_robot(miner, path[0]):
                        pass
                        # print("Moved", miner)
                # else:
                #     print("Charging")
            else:
                if rname not in self.miner_goal.keys():
                    # print("Miner finding mine", miner)
                    # take a remaining mine
                    best_mine = (None, -1)
                    for mine in remaining_mines:
                        path = game_state.optimal_path(miner.row, miner.col, mine[0], mine[1], checkCollisions=False)
                        if path[0] is not None:
                            if best_mine[0] is None or path[1] < best_mine[1]:
                                best_mine = (mine, path[1])
                    if best_mine[0] is not None:
                        self.miner_goal[rname] = (best_mine[0][0], best_mine[0][1])
                        remaining_mines.remove(best_mine[0])
                        # print("Found mine", miner, best_mine[0])
                if rname in self.miner_goal.keys():
                    goal = self.miner_goal[rname]
                    if not (miner.row == goal[0] and miner.col == goal[1]):
                        path = game_state.optimal_path(miner.row, miner.col, goal[0], goal[1], checkCollisions=True)
                        if path[0] is not None and self.move_robot(miner, path[0]):
                            pass
                        else:
                            if self.print_warnings:
                                print("Miner stuck", miner, goal)
                            # randomly move with probability 10% to prevent blocking other robots
                            if random.randint(0, 9) == 0:
                                valid_dirs = []
                                for dir in Direction:
                                    row = miner.row + dir.value[0]
                                    col = miner.col + dir.value[1]
                                    if row < 0 or row >= height or col < 0 or col >= width:
                                        continue
                                    if self.map[row][col] is None:  # fogged
                                        continue
                                    if self.map[row][col].robot is not None and self.map[row][
                                        col].robot.team == self.team:
                                        continue
                                    if self.map[row][col].state in [TileState.ILLEGAL, TileState.IMPASSABLE]:
                                        continue
                                    valid_dirs += [dir]
                                if len(valid_dirs) > 0:
                                    self.move_robot(miner, random.choice(valid_dirs))
                    if miner.row == goal[0] and miner.col == goal[1]:
                        if game_state.can_robot_action(rname):  # mine
                            game_state.robot_action(rname)
        if self.print_time:
            print("Time after moving miners:", time.time() - self.begin_time)

        # move bombs
        if len(my_bombs) > 0 and len(opponent_mines) > 0:
            for bomb in my_bombs:
                rname = bomb.name
                if self.map[bomb.row][bomb.col].state == TileState.MINING and \
                    self.count_mine_terraform(bomb.row, bomb.col) < 0:  # already at good location
                    continue
                for dir in Direction: # try to bomb opponent robots nearby
                    row = bomb.row + dir.value[0]
                    col = bomb.col + dir.value[1]
                    if row < 0 or row >= height or col < 0 or col >= width:
                        continue
                    if self.map[row][col] is None:  # fogged
                        continue
                    if self.map[row][col].robot is None or self.map[row][col].robot.team == self.team:
                        continue
                    if self.map[row][col].robot.battery < 20:  # not worth it
                        continue
                    if self.move_robot(bomb, dir):
                        break
                if not bomb.moved:
                    for mine in opponent_mines:
                        if self.map[mine[0]][mine[1]].robot is not None and \
                                self.map[mine[0]][mine[1]].robot.team == self.team:  # not bomb myself
                            continue
                        path = game_state.optimal_path(bomb.row, bomb.col, mine[0], mine[1], checkCollisions=True)
                        if path[0] is not None and self.move_robot(bomb, path[0]):  # bomb the mine
                            break
                        for goal_dir in Direction:
                            row = mine[0] + goal_dir.value[0]
                            col = mine[1] + goal_dir.value[1]
                            if row < 0 or row >= height or col < 0 or col >= width:
                                continue
                            if self.map[row][col] is None:  # fogged
                                continue
                            path = game_state.optimal_path(bomb.row, bomb.col, row, col, checkCollisions=True)
                            if path[0] is None:
                                continue
                            if self.move_robot(bomb, path[0]):
                                break
                if not bomb.moved:
                    # randomly move with probability 20% to prevent blocking other robots
                    if random.randint(0, 4) == 0:
                        valid_dirs = []
                        for dir in Direction:
                            row = bomb.row + dir.value[0]
                            col = bomb.col + dir.value[1]
                            if row < 0 or row >= height or col < 0 or col >= width:
                                continue
                            if self.map[row][col] is None:  # fogged
                                continue
                            if self.map[row][col].robot is not None and self.map[row][col].robot.team == self.team:
                                continue
                            if self.map[row][col].state in [TileState.ILLEGAL, TileState.IMPASSABLE]:
                                continue
                            valid_dirs += [dir]
                        if len(valid_dirs) > 0:
                            self.move_robot(bomb, random.choice(valid_dirs))
        if self.print_time:
            print("Time after moving bombs:", time.time() - self.begin_time)

        kSecureMinesTurns = 100
        # move terraformers
        # print("My terraformers", my_terraformers)
        self.compute_reachable_by_terraformers(my_terraformers)
        for row in range(height):
            for col in range(width):
                tile = self.map[row][col]
                if tile is not None and tile.state == TileState.MINING and self.reachable_by_terraformers[row][col] is not None:
                    insecure_count = self.count_mine_insecure(row, col)
                    if insecure_count > 0:
                        if insecure_count not in insecure_mines.keys():
                            insecure_mines[insecure_count] = []
                        insecure_mines[insecure_count] += [(tile.row, tile.col)]
        select_secure_mines = False
        if ginfo.turn < kSecureMinesTurns:
            # greedily secure the mines
            for insecure_count, mines in insecure_mines.items():
                for mine in mines:
                    for dir in Direction:
                        r = mine[0] + dir.value[0]
                        c = mine[1] + dir.value[1]
                        if r < 0 or r >= height or c < 0 or c >= width:
                            continue
                        if self.map[r][c] is None:
                            continue
                        if self.map[r][c].state != TileState.TERRAFORMABLE:
                            continue
                        if self.map[r][c].robot is not None:
                            continue
                        if self.map[r][c].terraform > 0:
                            continue
                        if self.reachable_by_terraformers[r][c] is None:
                            continue
                        terraformer = self.reachable_by_terraformers[mine[0]][mine[1]]
                        if terraformer.moved or terraformer.acted:
                            continue
                        if terraformer.battery < 20:
                            continue
                        if self.map[terraformer.row][terraformer.col].state == TileState.TERRAFORMABLE and \
                                self.map[terraformer.row][terraformer.col].terraform <= 0:
                            continue
                        rname = terraformer.name
                        path = game_state.optimal_path(terraformer.row, terraformer.col, r, c, checkCollisions=True)
                        if path[0] is not None and self.move_robot(terraformer, path[0]):
                            self.terraformer_charging[rname] = False # avoid charging again before reaching the destination
                            # no need to break -- we're not working on one robot
                            if self.map[terraformer.row][terraformer.col] is not None and \
                                    self.map[terraformer.row][terraformer.col].terraform <= 9 and \
                                    (self.map[terraformer.row][terraformer.col].terraform <= 0 or terraformer.battery >= 100):
                                if game_state.can_robot_action(rname):
                                    game_state.robot_action(rname)
                                    terraformer.acted = True

        if self.last_secure_mines_time < 0.5 and ginfo.turn < kSecureMinesTurns and \
                len(my_terraformers) <= len(insecure_mines) * 2:  # never secure mines again if any time it takes > 0.5 seconds
            start_secure_mines_time = time.time()
            select_secure_mines = True
            exclusions = set()
            for terraformer in my_terraformers:
                rname = terraformer.name
                if terraformer.moved or terraformer.acted:
                    continue
                bomb = self.try_to_bomb_opponent_mining(terraformer)
                if bomb is not None:
                    if self.map[terraformer.row][terraformer.col].state == TileState.TERRAFORMABLE and \
                            self.map[terraformer.row][terraformer.col].terraform <= 9 and game_state.can_robot_action(rname):
                        game_state.robot_action(rname)  # last terraform before sacrificing
                    if self.move_robot(terraformer, bomb):
                        continue
                    else:
                        if self.print_warnings:
                            print("Failed to bomb using terraformer!")
                        continue
                if terraformer.battery < 20:
                    self.terraformer_charging[rname] = True
                if terraformer.battery >= 100:
                    self.terraformer_charging[rname] = False
                if self.terraformer_charging[rname]:
                    if self.map[terraformer.row][terraformer.col].terraform <= 0:  # not charging
                        path = game_state.robot_to_base(rname, checkCollisions=True)
                        if path[0] is not None and self.move_robot(terraformer, path[0]):
                            pass
                else:
                    moved = False
                    best_goal = (None, -1)
                    if self.map[terraformer.row][terraformer.col].state == TileState.TERRAFORMABLE and \
                            self.map[terraformer.row][terraformer.col].terraform <= 0:
                        if game_state.can_robot_action(rname):
                            game_state.robot_action(rname)
                            terraformer.acted = True
                            continue
                    # move
                    try_count = 0
                    for insecure_count, mines in insecure_mines.items():
                        for mine in mines:
                            # check reachable
                            try_count += 1
                            path = game_state.optimal_path(terraformer.row, terraformer.col, mine[0], mine[1], checkCollisions=False)
                            if path[0] is None:
                                if self.debug:
                                    print("Unreachable from", terraformer, "to", mine)
                                continue
                            for dir in Direction:
                                r = mine[0] + dir.value[0]
                                c = mine[1] + dir.value[1]
                                if r < 0 or r >= height or c < 0 or c >= width:
                                    continue
                                if self.map[r][c] is None:
                                    continue
                                if self.map[r][c].state != TileState.TERRAFORMABLE:
                                    continue
                                if self.map[r][c].robot is not None:
                                    continue
                                if self.map[r][c].terraform > 0:
                                    continue
                                if self.reachable_by_terraformers[r][c] is None:
                                    continue
                                try_count += 1
                                path = game_state.optimal_path(terraformer.row, terraformer.col, r, c, checkCollisions=True)
                                if path[0] is not None:
                                    if best_goal[0] is None or path[1] < best_goal[2]:
                                        best_goal = ((r, c), (insecure_count, mine), path[1])
                                    if path[1] <= 1:
                                        break
                                else:
                                    if self.debug:
                                        print("Unreachable 2 from", terraformer, "to", mine)
                                    pass
                        if best_goal[0] is not None:
                            goal = (best_goal[0][0], best_goal[0][1])
                            path = game_state.optimal_path(terraformer.row, terraformer.col, goal[0], goal[1], checkCollisions=True)
                            if path[0] is not None and self.move_robot(terraformer, path[0]):
                                moved = True
                                break
                    if self.debug:
                        print("Tried", try_count, best_goal)
                    if moved:
                        insecure_count, mine = best_goal[1]
                        insecure_mines[insecure_count].remove(mine)
                    if moved and not terraformer.acted and self.map[terraformer.row][terraformer.col] is not None and \
                            self.map[terraformer.row][terraformer.col].terraform <= 9 and \
                            (self.map[terraformer.row][terraformer.col].terraform <= 0 or terraformer.battery >= 100):
                        if game_state.can_robot_action(rname):
                            game_state.robot_action(rname)
                            terraformer.acted = True
                    if not moved and not terraformer.acted:
                        # fall back to terraform anything
                        path = self.optimal_terraform(terraformer.row, terraformer.col,
                                                      exclusions, checkCollisions=True)
                        if path[0] is not None and self.move_robot(terraformer, path[0]):
                            exclusions.add(path[1])
                            moved = True
                        if not terraformer.acted and self.map[terraformer.row][terraformer.col] is not None and \
                                self.map[terraformer.row][terraformer.col].terraform <= 9 and \
                                (self.map[terraformer.row][terraformer.col].terraform <= 0 or terraformer.battery >= 100):
                            if game_state.can_robot_action(rname):
                                game_state.robot_action(rname)
                        if not terraformer.moved:
                            # randomly move with probability 20% to prevent blocking other robots
                            if random.randint(0, 4) == 0:
                                valid_dirs = []
                                for dir in Direction:
                                    row = terraformer.row + dir.value[0]
                                    col = terraformer.col + dir.value[1]
                                    if row < 0 or row >= height or col < 0 or col >= width:
                                        continue
                                    if self.map[row][col] is None:  # fogged
                                        continue
                                    if self.map[row][col].robot is not None and self.map[row][col].robot.team == self.team:
                                        continue
                                    if self.map[row][col].state in [TileState.ILLEGAL, TileState.IMPASSABLE]:
                                        continue
                                    valid_dirs += [dir]
                                if len(valid_dirs) > 0:
                                    self.move_robot(terraformer, random.choice(valid_dirs))

            end_secure_mines_time = time.time()
            self.last_secure_mines_time = end_secure_mines_time - start_secure_mines_time
        else:  # terraform as much as we can
            exclusions = set()
            for terraformer in my_terraformers:
                if terraformer.moved or terraformer.acted:
                    continue
                rname = terraformer.name
                if terraformer.battery < 20:
                    self.terraformer_charging[rname] = True
                if terraformer.battery >= 100:
                    self.terraformer_charging[rname] = False
                if terraformer.battery // 20 >= 200 - ginfo.turn:  # at the end
                    self.terraformer_charging[rname] = False
                if self.terraformer_charging[rname]:
                    if self.map[terraformer.row][terraformer.col].terraform <= 0:  # not charging
                        path = game_state.robot_to_base(rname, checkCollisions=True)
                        if path[0] is not None and self.move_robot(terraformer, path[0]):
                            pass
                else:
                    if not terraformer.acted and self.map[terraformer.row][terraformer.col] is not None and \
                            self.map[terraformer.row][terraformer.col].terraform <= 0:
                        if game_state.can_robot_action(rname):
                            game_state.robot_action(rname)
                            terraformer.acted = True
                    path = self.optimal_terraform(terraformer.row, terraformer.col,
                                                  exclusions, checkCollisions=True)
                    if path[0] is not None and self.move_robot(terraformer, path[0]):
                        exclusions.add(path[1])
                        moved = True
                    if not terraformer.acted and self.map[terraformer.row][terraformer.col] is not None and \
                            self.map[terraformer.row][terraformer.col].terraform <= 9 and \
                            (self.map[terraformer.row][terraformer.col].terraform <= 0 or terraformer.battery >= 100):
                        if game_state.can_robot_action(rname):
                            game_state.robot_action(rname)
                    if not terraformer.moved:
                        # randomly move with probability 20% to prevent blocking other robots
                        if random.randint(0, 4) == 0:
                            valid_dirs = []
                            for dir in Direction:
                                row = terraformer.row + dir.value[0]
                                col = terraformer.col + dir.value[1]
                                if row < 0 or row >= height or col < 0 or col >= width:
                                    continue
                                if self.map[row][col] is None:  # fogged
                                    continue
                                if self.map[row][col].robot is not None and self.map[row][col].robot.team == self.team:
                                    continue
                                if self.map[row][col].state in [TileState.ILLEGAL, TileState.IMPASSABLE]:
                                    continue
                                valid_dirs += [dir]
                            if len(valid_dirs) > 0:
                                self.move_robot(terraformer, random.choice(valid_dirs))

        if self.print_time:
            print("Time after moving terraformers (select_secure_mines =", select_secure_mines, "):",
                  time.time() - self.begin_time)

        # spawn locations
        for row in range(height):
            for col in range(width):
                # get the tile at (row, col)
                tile = self.map[row][col]
                # skip fogged tiles
                if tile is not None:  # ignore fogged tiles
                    if tile.robot is None and tile.terraform > 0:  # ensure tile is ally-terraformed
                        ally_tiles += [(row, col)]

        kOneExplorerBotDistanceThreshold = 2
        num_blocks = self.compute_connectivity()
        kExpectedNumExplorers = min(num_blocks + 2, 10)
        if ginfo.turn == 0:
            kExpectedNumExplorers = min(num_blocks, 10)
            if kExpectedNumExplorers < 10 and len(ally_tiles) - kExpectedNumExplorers > 1:
                kExpectedNumExplorers += 1
        if len(remaining_mines) > 0 and kExpectedNumExplorers > num_blocks:
            kExpectedNumExplorers = min(kExpectedNumExplorers,
                                        max(num_blocks,
                                            game_state.get_metal() // game_state.get_spawn_cost() - len(remaining_mines)))
        if self.total_fog_count / self.height / self.width > 0.1:
            kExpectedNumExplorers += len(my_terraformers) // 5
        blocks_to_spawn = set()
        for i in range(num_blocks):
            blocks_to_spawn.add(i)
        for explorer in my_explorers:
            if self.connectivity[explorer.row][explorer.col] is not None and \
                    self.connectivity[explorer.row][explorer.col] in blocks_to_spawn:
                blocks_to_spawn.remove(self.connectivity[explorer.row][explorer.col])
        # try to spawn explorers
        if len(ally_tiles) > 0 and len(fogged_tiles) > 0 and len(my_explorers) < kExpectedNumExplorers and \
                game_state.get_metal() >= game_state.get_spawn_cost():
            spawn_num = min(min(kExpectedNumExplorers - len(my_explorers),
                            game_state.get_metal() // game_state.get_spawn_cost()),
                            len(ally_tiles))
            for spawn_loc in ally_tiles:
                if spawn_num <= 0 or len(blocks_to_spawn) == 0:
                    break
                if self.connectivity[spawn_loc[0]][spawn_loc[1]] in blocks_to_spawn:
                    # spawn the robot
                    spawn_type = RobotType.EXPLORER
                    if self.debug:
                        print(f"Spawning explorer at {spawn_loc[0], spawn_loc[1]}")
                    # check if we can spawn here (checks if we can afford, tile is empty, and tile is ours)
                    if game_state.can_spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1]):
                        robot = game_state.spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1])
                        ally_tiles.remove(spawn_loc)
                        spawn_num -= 1
                        blocks_to_spawn.remove(self.connectivity[spawn_loc[0]][spawn_loc[1]])
                    else:
                        if self.print_warnings:
                            print(f"Failed to spawn explorer for connectivity!")

            exploring_fogged_tiles = fogged_tiles
            for i in range(spawn_num):
                min_fog_distances = [(min([coord_distance(co, ally_tile) for co in exploring_fogged_tiles]), ally_tile)
                                     for ally_tile in ally_tiles]
                min_fog_distance = min_fog_distances[0]
                for tmp in min_fog_distances:
                    if tmp[0] < min_fog_distance[0]:
                        min_fog_distance = tmp
                spawn_loc = min_fog_distance[1]
                spawn_type = RobotType.EXPLORER
                goal = None
                new_exploring_fogged_tiles = []
                for tile in exploring_fogged_tiles:
                    if coord_distance(spawn_loc, tile) == min_fog_distance[0]:
                        goal = tile
                    elif coord_distance(spawn_loc, tile) > min_fog_distance[0] + kOneExplorerBotDistanceThreshold:
                        new_exploring_fogged_tiles += [tile]
                # spawn the robot
                if self.debug:
                    print(f"Spawning explorer at {spawn_loc[0], spawn_loc[1]} with goal {goal[0], goal[1]}")
                # check if we can spawn here (checks if we can afford, tile is empty, and tile is ours)
                if game_state.can_spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1]):
                    robot = game_state.spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1])
                    self.explorer_goal[robot.name] = goal
                    ally_tiles.remove(spawn_loc)
                else:
                    if self.print_warnings:
                        print(f"Failed to spawn explorer!")
                exploring_fogged_tiles = new_exploring_fogged_tiles
                if len(exploring_fogged_tiles) == 0:
                    break
        if self.print_time:
            print("Time after spawning explorers:", time.time() - self.begin_time)

        kMinTerraformerPerHowManyMiners = 3
        # try to spawn miners
        if len(ally_tiles) > 0 and len(remaining_mines) > 0 and game_state.get_metal() >= game_state.get_spawn_cost():
            spawn_num = min(min(len(remaining_mines),
                                game_state.get_metal() // game_state.get_spawn_cost()),
                            len(ally_tiles))
            if len(my_miners) + spawn_num >= kMinTerraformerPerHowManyMiners * (len(my_terraformers) + 1):
                spawn_num -= (len(my_miners) + spawn_num - kMinTerraformerPerHowManyMiners * len(my_terraformers)) \
                             // (kMinTerraformerPerHowManyMiners + 1)
                if spawn_num < 0:
                    spawn_num = 0
            for mine in remaining_mines:
                if spawn_num <= 0:
                    break
                ally_tile_set = set([(tile[0], tile[1]) for tile in ally_tiles])
                spawn_loc = self.closest_ally_tile_to_spawn(mine[0], mine[1], ally_tile_set)
                if spawn_loc[0] is None:
                    if self.print_warnings:
                        print("Failed to select spawn location for mine", mine, ally_tiles)
                    spawn_loc = random.choice(ally_tiles)
                spawn_type = RobotType.MINER
                # spawn the robot
                if self.debug:
                    print(f"Spawning miner at {spawn_loc[0], spawn_loc[1]} with goal {mine}")
                # check if we can spawn here (checks if we can afford, tile is empty, and tile is ours)
                if game_state.can_spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1]):
                    robot = game_state.spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1])
                    self.miner_charging[robot.name] = False
                    self.miner_goal[robot.name] = mine
                    ally_tiles.remove(spawn_loc)
                    spawn_num -= 1
                else:
                    if self.print_warnings:
                        print(f"Failed to spawn miner!")
        if self.print_time:
            print("Time after spawning miners:", time.time() - self.begin_time)

        # try to spawn terraformers
        if len(ally_tiles) > 0 and game_state.get_metal() >= game_state.get_spawn_cost():
            spawn_num = min(game_state.get_metal() // game_state.get_spawn_cost(),
                            len(ally_tiles))
            for i in range(spawn_num):
                spawn_loc = random.choice(ally_tiles)  # TODO
                spawn_type = RobotType.TERRAFORMER
                # spawn the robot
                if self.debug:
                    print(f"Spawning terraformer at {spawn_loc[0], spawn_loc[1]}")
                # check if we can spawn here (checks if we can afford, tile is empty, and tile is ours)
                if game_state.can_spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1]):
                    robot = game_state.spawn_robot(spawn_type, spawn_loc[0], spawn_loc[1])
                    self.terraformer_charging[robot.name] = False
                    ally_tiles.remove(spawn_loc)
                else:
                    if self.print_warnings:
                        print(f"Failed to spawn terraformer!")
        if self.print_time:
            print("Time after spawning terraformers:", time.time() - self.begin_time)


#!/bin/python3

class Op:
    def __init__(self, f):
        self.f = f

class BinOp(Op):
    pass

class Word:
    def __init__(self, name, values):
        self.name = name
        self.values = values

glossary = {
    '+' : BinOp(lambda args: args[0] + args[1]),
    '-' : BinOp(lambda args: args[0] - args[1]),
    '/' : BinOp(lambda args: args[0] / args[1]),
    '*' : BinOp(lambda args: args[0] * args[1])
}

stack = []
# debug = True
debug = False
NONE = 0
DEF = 1

def error(msg):
    print("Error: {}".format(msg))

def push(v):
    stack.append(v)

def pop():
    return stack.pop()

def peek():
    return stack[-1]

def repl():
    i = input()
    tokens = i.split(' ')
    result = 0
    state = NONE

    for t in tokens:
        state = ev(t, state)
        if result < 0:
            break

    if result == 0:
        print ("ok")

    if debug:
        print("STACK")
        print(stack)
        print("DICTIONARY")
        print(glossary)
        print('\n\n\n')

def ev(t, state):
    if t == '':
        return state

    if t == '.':
        print(pop())
        return state

    try:
        v = int(t)
        push(v)
        return state
    except:
        pass

    if ':' == t:
        state = DEF
        push(':')
        return DEF

    if ';' == t:
        if DEF == state:
            vals = [pop()]

            while vals[-1] != ':':
                vals.append(pop())

            define(vals[-2], list(reversed(vals[0:-2])))
            return NONE
        else:
            error("Unexpected semicolon")
            return -2

    if DEF == state:
        push(t)

    elif t in glossary:
        word = glossary[t]

        if isinstance(word, Word):
            for v in word.values:
                ev(v, state)
        elif isinstance(word, BinOp):
            push(word.f([pop(), pop()]))

    else:
        error("Undefined word")
        return -1

    return state

def define(name, values):
    if name in glossary:
        error("Word already defined")
        return -1

    glossary[name] = Word(name, values)

while 1:
    repl()


from config import *
import os, shutil, stat
import numpy as np
import cv2

class Data_Generator():
    """
    this class contains method for generating synthetic data with and without circles in the images

    Attributes:
    perc_mix_images: float, percentage of images to be generated with mix shapes out of all the images
                    with circles
    perc_circle_images: float, percentage of images to be generated with circles
    mix_shape_generator: boolean, an boolean whether images of mixture of shapes is to be
                         created or not
    """

    def __init__(self, perc_mix_images= 0, perc_circle_images=0.5, add_ellipse_images = False):
        self.perc_mix_images = perc_mix_images
        self.perc_circle_images = perc_circle_images
        self.add_ellipse_images = add_ellipse_images

        # If the Images folder already exists, then delete the folder
        if 'Images' in os.listdir():
            shutil.rmtree(f"{os.getcwd()}\\Images", onerror=self.remove_read_only)

    def remove_read_only(self, func, path, excinfo):
        """
        method to remove read only files

        :param [func, path, excinfo]: various parameters for changing the mode
        """
        os.chmod(path, stat.S_IWRITE)
        func(path)

    def save_image(self, image, file_name):
        """
        method to save the image
        :param image: np.ndarray, a numpy array of image
        :param file_name: str, a string of the file name in which image is to be stored
        """

        # Create Image folder if it does not exist
        if not os.path.exists('Images'):
            os.makedirs('Images')
        cv2.imwrite(os.path.join(os.getcwd(), 'Images', file_name), image)

    def normalize_image(self, images):
        """
        method to normalize the images
        :param images: np.ndarray, a numpy array of all the generated images
        :return images_norm: np.ndarray, a numpy array of all the normalized images
        """

        return images / 255

    def circle_parameter_generator(self):
        """
        method to generate parameters for adding circle in the image

        :return [center_coordinates, radius, color, thickness]: list, a list of all the
                circle parameters
        """

        # Center coordinates generated randomly
        center_coordinates = (np.random.randint(15, 85), np.random.randint(15, 85))

        # Radius of circle generated randomly
        radius = np.random.randint(5, 25)

        # BGR setting generated randomly
        color = (np.random.randint(256), np.random.randint(256), np.random.randint(256))

        # Line thickness generated randomly
        thickness = np.random.randint(5)

        return center_coordinates, radius, color, thickness

    def rectangle_parameter_generator(self):
        """
        method to generate parameters for adding rectangle in the image

        :return [start_point, end_point, color, thickness]: list, a list of all the rectangle
                parameters
        """

        # Starting co-ordinate generated randomly
        start_point = (np.random.randint(3, 25), np.random.randint(3, 25))

        # Ending co-ordinate generated randomly
        end_point = (np.random.randint(30, 85), np.random.randint(30, 85))

        # BGR setting generated randomly
        color = (np.random.randint(256), np.random.randint(256), np.random.randint(256))

        # Line thickness generated randomly
        thickness = np.random.randint(5)

        return start_point, end_point, color, thickness

    def ellipse_parameter_generator(self):
        """
        method to generate parameters for adding ellipse in the image

        :return [center_coordinates, axesLength, angles, color, thickness]: list, a list of all
                the ellipse parameters
        """

        # Center coordinates generated randomly
        center_coordinates = (np.random.randint(15, 85), np.random.randint(15, 85))

        axesLength = (np.random.randint(3, 25), np.random.randint(3, 25))

        #  Defining angles as [angle, start angle, end angle]
        angles = [0, 0, 360]

        # BGR setting generated randomly
        color = (np.random.randint(256), np.random.randint(256), np.random.randint(256))

        # Line thickness generated randomly
        thickness = np.random.randint(5)

        return center_coordinates, axesLength, angles, color, thickness

    def triangle_parameter_generator(self):
        """
        method to generate parameters for adding triangle in the image

        :return [[p1, p2, p3], color, thickness]: list, a list of all
                the ellipse parameters
        """

        # Three vertices(tuples) of the triangle
        p1 = (np.random.randint(5, 15), np.random.randint(5, 15))
        p2 = (np.random.randint(65, 75), np.random.randint(25, 35))
        p3 = (np.random.randint(45, 75), np.random.randint(65, 85))

        # BGR setting generated randomly
        color = (np.random.randint(256), np.random.randint(256), np.random.randint(256))

        # Line thickness generated randomly
        thickness = np.random.randint(1, 4)

        return [p1, p2, p3], color, thickness

    def image_circle_generator(self):
        """
        method to generate an image with or without circles

        :return image: np.ndarray, a numpy array of image
        :return label: int, a label for indicating whether circle is present in the
                            image or not
        """

        # Create an Image of size (100, 100, 3)
        image = np.zeros((img_config['img_height'], img_config['img_height'],
                            img_config['no_channels']), np.uint8)

        # Get all the parameters
        center_coordinates, radius, color, thickness = self.circle_parameter_generator()

        # Draw a circle with random parameters
        image = cv2.circle(image, center_coordinates, radius, color, thickness)
        # 1 represents circle is present in the image
        label = 1

        return image, label

    def image_rectangle_generator(self):
        """
        method to generate an image with or without circles

        :return image: np.ndarray, a numpy array of image
        :return label: int, a label for indicating whether circle is present in the
                            image or not
        """

        image = np.zeros((img_config['img_height'], img_config['img_height'], 3), np.uint8)

        # Get all the parameters
        start_point, end_point, color, thickness = self.rectangle_parameter_generator()

        # Draw a rectangle with random parameters
        image = cv2.rectangle(image, start_point, end_point, color, thickness)
        # 0 represents circle is absent in the image
        label = 0

        return image, label

    def image_ellipse_generator(self):
        """
        method to generate an image with ellipse

        :return image: np.ndarray, a numpy array of image
        :return label: int, a label for indicating whether circle is present in the
                            image or not
        """

        # Create an Image of size (100, 100, 3)
        image = np.zeros((img_config['img_height'], img_config['img_height'],
                            img_config['no_channels']), np.uint8)

        # Get all the parameters
        center_coordinates, axesLength, angles, color, thickness = self.ellipse_parameter_generator()

        # Draw a circle with random parameters
        image = cv2.ellipse(image, center_coordinates, axesLength,
           angles[0], angles[1], angles[2], color, thickness)
        # 1 represents circle is present in the image
        label = 0

        return image, label

    def image_triangle_generator(self):
        """
        method to generate an image with triangle

        :return image: np.ndarray, a numpy array of image
        :return label: int, a label for indicating whether circle is present in the
                            image or not
        """

        # Create an Image of size (100, 100, 3)
        image = np.zeros((img_config['img_height'], img_config['img_height'],
                            img_config['no_channels']), np.uint8)

        # Get all the parameters
        points, color, thickness = self.triangle_parameter_generator()

        # Draw a circle with random parameters
        image = cv2.line(image, points[0], points[1], color, thickness)
        image = cv2.line(image, points[1], points[2], color, thickness)
        image = cv2.line(image, points[0], points[2], color, thickness)
        # 1 represents circle is present in the image
        label = 0

        return image, label

    def image_mix_generator(self):
        """
        method to generate an image with a mixture of circle and rectangle

        :return image: np.ndarray, a numpy array of image
        :return label: int, a label for indicating whether circle is present in the
                            image or not
        """

        image = np.zeros((img_config['img_height'], img_config['img_height'], 3), np.uint8)

        # Get all the parameters and draw a circle
        center_coordinates, radius, color, thickness = self.circle_parameter_generator()
        image = cv2.circle(image, center_coordinates, radius, color, thickness)

        # Ellipse inserted only if add_ellipse_images = True
        if self.add_ellipse_images:
            # Here flag = 2 represents a flag of inserting Ellipse
            flag = np.random.randint(3)
        else:
            flag = np.random.randint(2)
        if flag == 0:
            # If flag is 0, then insert Rectangle
            start_point, end_point, color, thickness = self.rectangle_parameter_generator()
            image = cv2.rectangle(image, start_point, end_point, color, thickness)
        elif flag == 1:
            # If flag is 1, then insert TrianglE
            # Get all the parameters and insert Triangle
            points, color, thickness = self.triangle_parameter_generator()
            # Draw a circle with random parameters
            image = cv2.line(image, points[0], points[1], color, thickness)
            image = cv2.line(image, points[1], points[2], color, thickness)
            image = cv2.line(image, points[0], points[2], color, thickness)
        else:
            # If flag is "2", then insert Ellipse
            center_coordinates, axesLength, angles, color, thickness = self.ellipse_parameter_generator()
            image = cv2.ellipse(image, center_coordinates, axesLength,
            angles[0], angles[1], angles[2], color, thickness)

        # 1 represents circle is present in the image
        label = 1

        return image, label

    def image_generator(self):
        """
        method to generate no_img images

        :return image_array: np.ndarray, a numpy array of all the generated images
        :return label_array: list, a list of all the labels
        """

        no_images_with_circle = int(img_config['no_img'] * self.perc_circle_images)
        no_mix_images = int(no_images_with_circle * self.perc_mix_images)

        image_array = np.zeros((img_config['no_img'], img_config['img_height'],
                                img_config['img_width'], img_config['no_channels']))
        label_array = []
        print('Total Images to be created is:', img_config['no_img'])
        print('Images with circle is:', no_images_with_circle)
        print('Images with mix shapes and circle is:', no_mix_images)
        print('Creating Images...')
        for idx in range(img_config['no_img']):
            # Create Images according to the percentages given by the user
            if idx < no_mix_images:
                image, label = self.image_mix_generator()
            elif idx < no_images_with_circle:
                image, label = self.image_circle_generator()
            else:
                # Ellipse inserted only if add_ellipse_images = True
                if self.add_ellipse_images:
                    # Here flag = 2 represents a flag of inserting Ellipse
                    flag = np.random.randint(3)
                else:
                    flag = np.random.randint(2)
                if flag == 0:
                    # If flag is 0, then insert Rectangle
                    image, label = self.image_rectangle_generator()
                elif flag == 1:
                    # If flag is 1, then insert Triangle
                    image, label = self.image_triangle_generator()
                else:
                     # If flag is 2, then insert Ellipse
                    image, label = self.image_ellipse_generator()

            file_name = f"Img_{str(idx).zfill(4)}.png"
            self.save_image(image, file_name)
            image_array[idx] = image
            label_array.append(label)
        print('Images created and saved in directory as below:-')
        print(os.getcwd())
        return self.normalize_image(image_array), label_array


import math
import random

xmin = -50.0
xmax = 50.0

ymin = -50.0
ymax = 50.0

disc_value = 25.0

def g(x, y):
    #
    # A Square
    #
    global xmin, xmax, ymin, ymax, disc_value

    R = min([(xmax - xmin), (ymax - ymin)])/4.0
    x0 = (xmax + xmin)/2.0
    y0 = (ymax + ymin)/2.0

    dx = math.fabs(x - x0)
    dy = math.fabs(y - y0)

    if (dx < R) and (dy < R):
        return disc_value
    else:
        return 0.0

if __name__ == '__main__':

    noise = 2.0
    nsamplepoints = 500

    xt = [random.uniform(xmin, xmax) for i in range(nsamplepoints)]
    yt = [random.uniform(ymin, ymax) for i in range(nsamplepoints)]

    data = [(x, y, g(x, y) + random.normalvariate(0.0, noise)) for (x, y) in zip(xt, yt)]

    f = open('data.txt', 'w')
    f.write('\n'.join(map(lambda x: ' '.join(map(str, x)), data)))
    f.close()


import argparse
import numpy as np
from smac.env import StarCraft2Env

# import whatever you need to defined and load your trained model
##########################################################################
from learners import REGISTRY as le_REGISTRY
from runners import REGISTRY as r_REGISTRY
from controllers import REGISTRY as mac_REGISTRY
from main import recursive_dict_update, _get_config
from components.transforms import OneHot
from types import SimpleNamespace as SN
from copy import deepcopy
from utils.logging import get_logger
from components.episode_buffer import ReplayBuffer
import sys
import os
import yaml
import torch as th
##########################################################################

def test_model(model, env, num_runs=50):
    """
    :param model:
    :param env:
    :param num_runs:
    :return:
    """
    wins = []
    for i in range(num_runs):

        ############### you can design your test routines on here. ###############
        # A routine of yours to infer the action(s) from the state or observation
        # actions = model(state)

        print(wins)
        terminated = False

        learner = model
        runner = learner.runner_for_test

        runner.batch = runner.new_batch()
        env.reset()
        runner.t = 0

        episode_return = 0
        learner.mac.init_hidden(batch_size=runner.batch_size)

        while not terminated:
            pre_transition_data = {
                "state": [env.get_state()],
                "avail_actions": [env.get_avail_actions()],
                "obs": [env.get_obs()]
            }

            runner.batch.update(pre_transition_data, ts=runner.t)

            # Pass the entire batch of experiences up till now to the agents
            # Receive the actions for each agent at this timestep in a batch of size 1
            actions, _, _, _ = learner.mac.select_actions_comm_proto(
                runner.batch, t_ep=runner.t, t_env=runner.t, test_mode=True)

            # ========================================================================
            # ====Moved this Code from outside the loop into the loop=================
            reward, terminated, env_info = env.step(actions[0])
            # ========================================================================
            # ========================================================================

            episode_return += reward

            post_transition_data = {
                "actions": actions[0],
                "reward": [(reward,)],
                "terminated": [(terminated != env_info.get("episode_limit", False),)],
            }

            runner.batch.update(post_transition_data, ts=runner.t)

            runner.t += 1

        last_data = {
            "state": [env.get_state()],
            "avail_actions": [env.get_avail_actions()],
            "obs": [env.get_obs()]
        }
        runner.batch.update(last_data, ts=runner.t)

        # Select actions in the last stored state
        actions, _, _, _ = learner.mac.select_actions_comm_proto(
                runner.batch, t_ep=runner.t, t_env=runner.t, test_mode=True)
        runner.batch.update({"actions": actions}, ts=runner.t)
        info = env_info
        if info['battle_won'] is False:
            print("HERE")
        ##########################################################################

        # ===The following line is moved into "While" loop=========================
        # reward, terminated, info = env.step(actions)
        # =========================================================================

        if terminated:
            win = True if info['battle_won'] else False
        wins.append(float(win))

        env.save_replay()
        # import time
        # time.sleep(0.5)

    return np.average(wins)

def load_model(model_path):
    """
    :param model_path:
    :return: the loaded model (an instance of torch.nn.Module expected)
    """

    """
        do whatever you want to load model.
        In the end of this code block, we expect to get loaded_model = model
    """
    ############### you can design your test routines on here. ###############
    params = ['--config=qmix_8m', '--env-config=sc2']
    logger = get_logger()

    # Get the defaults from default.yaml
    with open(os.path.join(os.path.dirname(__file__), "config", "default.yaml"), "r") as f:
        try:
            config_dict = yaml.load(f)
        except yaml.YAMLError as exc:
            assert False, "default.yaml error: {}".format(exc)

    # Load algorithm and env base configs
    env_config = _get_config(params, "--env-config", "envs")
    alg_config = _get_config(params, "--config", "algs")
    config_dict = recursive_dict_update(config_dict, env_config)
    config_dict = recursive_dict_update(config_dict, alg_config)
    if config_dict["use_cuda"] and not th.cuda.is_available():
        config_dict["use_cuda"] = False
    config_dict['env_args']['map_name'] = config_dict['mac'][10:]
    config_dict['runner'] = 'episode'
    config_dict['batch_size_run'] = 1
    args = SN(**config_dict)
    args.device = "cuda" if args.use_cuda else "cpu"

    # Init runner so we can get env info
    runner = r_REGISTRY[args.runner](args=args, logger=logger)

    # Set up schemes and groups here
    env_info = runner.get_env_info()
    args.n_agents = env_info["n_agents"]
    args.n_actions = env_info["n_actions"]
    args.state_shape = env_info["state_shape"]

    # Default/Base scheme
    scheme = {
        "state": {"vshape": env_info["state_shape"]},
        "obs": {"vshape": env_info["obs_shape"], "group": "agents"},
        "actions": {"vshape": (1,), "group": "agents", "dtype": th.long},
        "avail_actions": {"vshape": (env_info["n_actions"],), "group": "agents", "dtype": th.int},
        "reward": {"vshape": (1,)},
        "terminated": {"vshape": (1,), "dtype": th.uint8},
    }
    groups = {
        "agents": args.n_agents
    }
    preprocess = {
        "actions": ("actions_onehot", [OneHot(out_dim=args.n_actions)])
    }

    buffer = ReplayBuffer(scheme, groups, args.buffer_size, env_info["episode_limit"] + 1,
                          preprocess=preprocess,
                          device="cpu" if args.buffer_cpu_only else args.device)

    # Setup multiagent controller here
    mac = mac_REGISTRY[args.mac](buffer.scheme, groups, args)

    # Give runner the scheme
    runner.setup(scheme=scheme, groups=groups, preprocess=preprocess, mac=mac)

    # Learner
    learner = le_REGISTRY[args.learner](mac, buffer.scheme, logger, args)
    learner.get_runner(runner)

    if args.use_cuda:
        learner.cuda()

    learner.load_models(model_path)
    ##########################################################################

    loaded_model = learner
    return loaded_model

def main(model_path):
    """
    Todo
    1) modify the difficulty setting
    2) window_size settings
    """
    env = StarCraft2Env(map_name="8m",
                        difficulty="9",
                        window_size_x=1920/3,
                        window_size_y=1200/3)
    loaded_model = load_model(model_path)
    mean_wr = test_model(loaded_model, env, num_runs=50)
    return mean_wr

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--path', required=True, help='path to your model')
    args = parser.parse_args()
    mean_wr = main(args.path)

    print("Your average winning rate is {}".format(mean_wr))


MENU = {
    "espresso": {
        "ingredients": {
            "water": 50,
            "coffee": 18,
        },
        "cost": 120,
    },
    "latte": {
        "ingredients": {
            "water": 200,
            "milk": 150,
            "coffee": 24,
        },
        "cost": 200,
    },
    "cappuccino": {
        "ingredients": {
            "water": 250,
            "milk": 100,
            "coffee": 24,
        },
        "cost": 240,
    }
}

resources = {
    "water": 300,
    "milk": 200,
    "coffee": 100,
}

user_coffee = ''
total_money = 0
req_water = 0
req_milk = 0
req_coffee = 0

def refill():
    resources["water"] = 300
    resources["milk"] = 200
    resources["coffee"] = 100
    coffee_machine()

def report():
    print(f"Water:{resources['water']}ml")
    print(f"milk:{resources['milk']}ml")
    print(f"coffee:{resources['coffee']}g")
    print(f"Money: {total_money}rs")
    coffee_machine()

def coffee_machine():
    global user_coffee
    global req_water
    global req_coffee
    global req_milk

    user_coffee = input("What Would You Like? (espresso/latte/cappuccino):")

    if user_coffee == "report":
        report()
    elif user_coffee == "refill":
        refill()
    elif user_coffee == "off":
        print("coffee machine is off")
        return
    else:
        print(f"{user_coffee} is of {MENU[user_coffee]['cost']}rs.")
        req_water = MENU[user_coffee]["ingredients"]["water"]
        if user_coffee != "espresso":
            req_milk = MENU[user_coffee]["ingredients"]["milk"]
        req_coffee = MENU[user_coffee]["ingredients"]["coffee"]

        if req_coffee <= resources["coffee"]:
            if req_water <= resources["water"]:
                if user_coffee != "espresso":
                    if req_milk <= resources["milk"]:
                        resources["water"] -= req_water
                        resources["coffee"] -= req_coffee
                        resources["milk"] -= req_milk
                        payment()
                    else:
                        print("not enough milk")
                        coffee_machine()
                else:
                    resources["coffee"] -= req_coffee
                    resources["water"] -= req_water
                    payment()

            else:
                print("not enough water")
                coffee_machine()
        else:
            print("not enough coffee")
            coffee_machine()

# payment
# Remember that quarters = $0.25, dimes = $0.10, nickles = $0.05, pennies = $0.01
def payment():
    global user_coffee
    global total_money
    print("PLease Pay.")
    quarters = int(input("How many 200 ke note?:"))
    dimes = int(input("How many 100 ke note?:"))
    nickles = int(input("How many 50 ke note?:"))
    pennies = int(input("How many 10 ke note?:"))

    user_payment = (200 * quarters) + (100 * dimes) + (50 * nickles) + (10 * pennies)

    if user_payment >= MENU[user_coffee]["cost"]:
        total_money += MENU[user_coffee]["cost"]
        refund_money = user_payment - MENU[user_coffee]["cost"]
        print(f"Here is {refund_money}rs in change.")
        print(f"Here is you {user_coffee} enjoy.")
        coffee_machine()
    else:
        print("Sorry that is not enough money. money refunded")
        resources["water"] += req_water
        if user_coffee != "espresso":
            resources["milk"] += req_milk
        resources["coffee"] += req_coffee

        coffee_machine()

coffee_machine()


#BUBBLE SORT
arr=[3,70,2,53,6,48,95,31,1]
finish=False

while finish==False:
    finish=True
    for i in range(len(arr)-1):
        if arr[i]>arr[i+1]:
            assistant=arr[i]
            arr[i]=arr[i+1]
            arr[i+1]=assistant
            finish=False

print(arr)

#SELECTION SORT
    #De menor a mayor
arr=[3,70,2,53,6,48,95,31,1]

for i in range(len(arr)):
    minimum=i
    for j in range(i+1, len(arr)):
        if arr[minimum]>arr[j]:
            minimum=j

    assistant=arr[i]
    arr[i]=arr[minimum]
    arr[minimum]=assistant

print(arr)

    #De mayor a menor
arr=[3,70,2,53,6,48,95,31,1]

for i in range(len(arr)):
    maximum=i
    for j in range(i+1, len(arr)):
        if arr[maximum]<arr[j]:
            maximum=j
    assistant=arr[maximum]
    arr[maximum]=arr[i]
    arr[i]=assistant

print(arr)

#INSERTION SORT
arr=[3,70,2,53,6,48,95,31,1]

for i in range(1,len(arr)):
    key=arr[i]

    j=i-1
    while j>=0 and arr[j]>key:
        arr[j+1]=arr[j]
        j=j-1
        arr[j+1]=key

print(arr)

#MERGE SORT
def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    # Dividir la lista en dos mitades
    mid = len(arr) // 2
    left_half = arr[:mid]
    right_half = arr[mid:]

    # Ordenar recursivamente cada mitad
    left_half = merge_sort(left_half)
    right_half = merge_sort(right_half)

    # Combinar las mitades ordenadas
    return merge(left_half, right_half)

def merge(left, right):
    result = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])

    return result

# Ejemplo de uso
arr =[3,70,2,53,6,48,95,31,1]
sorted_arr = merge_sort(arr)
print(sorted_arr)


import numpy as np
import matplotlib.pyplot as plt

def read_jam_factor(path):
    last_time = 0
    data = []
    with open(path) as f:
        line = f.readline()
        while line:
            s = line.split(' ')
            s = [float(x) for x in s]
            t = s[0]

            if int(t) >= last_time + 30:
                data.append((t,
                    s[4] / s[1],
                    s[5] / s[2],
                    s[6] / s[3]))
                print(path)
                print(sum(s[4:7]) / sum(s[1:4]))
                last_time = int(t)
            line = f.readline()
    return data

data_gamma = read_jam_factor('statistics.gamma.log')
data_simple = read_jam_factor('statistics.ttc.log')

avg_total = 0
avg_count = 0
for x in data_gamma:
    avg_total += x[1] + x[2] + x[3]

fig, axs = plt.subplots(figsize=(17.5, 8))
axs.set_xlabel('Simulation Time (s)', fontsize=34)
axs.set_ylabel('Congestion Factor', fontsize=34)
plt.xlim(0, 1200)
plt.ylim(-0.01, 1.19)
plt.rc('legend',fontsize=26)
axs.tick_params(axis='x', labelsize=32)
axs.tick_params(axis='y', labelsize=32)

# GAMMA Car Jam Factor.
axs.plot(
        [x[0] for x in data_gamma],
        [x[1] for x in data_gamma],
        label='Car (C-GAMMA)',
        color='red',
        linestyle='solid',
        linewidth=3)

# TTC Car Jam Factor.
axs.plot(
        [x[0] for x in data_simple],
        [x[1] for x in data_simple],
        label='Car (TTC)',
        color='red',
        linestyle=(0, (5, 5)),
        linewidth=3)

# GAMMA Bike Jam Factor.
axs.plot(
        [x[0] for x in data_gamma],
        [x[2] for x in data_gamma],
        label='Bike (C-GAMMA)',
        color='green',
        linestyle='solid',
        linewidth=3)

# TTC Bike Jam Factor.
axs.plot(
        [x[0] for x in data_simple],
        [x[2] for x in data_simple],
        label='Bike (TTC)',
        color='green',
        linestyle=(0, (5, 5)),
        linewidth=3)

# GAMMA Ped. Jam Factor.
axs.plot(
        [x[0] for x in data_gamma],
        [x[3] for x in data_gamma],
        label='Pedestrian (C-GAMMA)',
        color='blue',
        linestyle='solid',
        linewidth=3)

# TTC Ped. Jam Factor.
axs.plot(
        [x[0] for x in data_simple],
        [x[3] for x in data_simple],
        label='Pedestrian (TTC)',
        color='blue',
        linestyle=(0, (5, 5)),
        linewidth=3)

axs.legend(loc="upper right", ncol=3)

fig.tight_layout()
plt.show()


import dep_check

# Python module imports.
from os import sep
PIPE, Popen = None, None
if dep_check.subprocess_module:
    from subprocess import PIPE, Popen

# relax module imports.
from lib.errors import RelaxError, RelaxNoSequenceError
from lib.io import mkdir_nofail, open_write_file
from lib.periodic_table import periodic_table
from pipe_control.mol_res_spin import exists_mol_res_spin_data, return_residue
from pipe_control.pipes import check_pipe
from specific_analyses.relax_disp.data import loop_cluster, loop_exp_frq, loop_offset_point, loop_time, return_param_key_from_data, spin_ids_to_containers

def sherekhan_input(spin_id=None, force=False, dir='ShereKhan'):
    """Create the ShereKhan input files.

    @keyword spin_id:           The spin ID string to restrict the file creation to.
    @type spin_id:              str
    @keyword force:             A flag which if True will cause all pre-existing files to be overwritten.
    @type force:                bool
    @keyword dir:               The optional directory to place the files into.  If None, then the files will be placed into the current directory.
    @type dir:                  str or None
    """

    # Test if the current pipe exists.
    check_pipe()

    # Test if sequence data is loaded.
    if not exists_mol_res_spin_data():
        raise RelaxNoSequenceError

    # Test if the experiment type has been set.
    if not hasattr(cdp, 'exp_type'):
        raise RelaxError("The relaxation dispersion experiment type has not been specified.")

    # Test if the model has been set.
    if not hasattr(cdp, 'model_type'):
        raise RelaxError("The relaxation dispersion model has not been specified.")

    # Directory creation.
    if dir != None:
        mkdir_nofail(dir, verbosity=0)

    # Loop over the spin blocks.
    cluster_index = 0
    for spin_ids in loop_cluster():
        # The spin containers.
        spins = spin_ids_to_containers(spin_ids)

        # Loop over the magnetic fields.
        for exp_type, frq, ei, mi in loop_exp_frq(return_indices=True):
            # Loop over the time, and count it.
            time_i = 0
            for time, ti in loop_time(exp_type=exp_type, frq=frq, return_indices=True):
                time_i += 1

            # Check that not more than one time point is returned.
            if time_i > 1:
                raise RelaxError("Number of returned time poins is %i. Only 1 time point is expected."%time_i)

            # The ShereKhan input file for the spin cluster.
            file_name = 'sherekhan_frq%s.in' % (mi+1)
            if dir != None:
                dir_name = dir + sep + 'cluster%s' % (cluster_index+1)
            else:
                dir_name = 'cluster%s' % (cluster_index+1)
            file = open_write_file(file_name=file_name, dir=dir_name, force=force)

            # The B0 field for the nuclei of interest in MHz (must be positive to be accepted by the server).
            file.write("%.10f\n" % abs(frq / periodic_table.gyromagnetic_ratio('1H') * periodic_table.gyromagnetic_ratio('15N') / 1e6))

            # The constant relaxation time for the CPMG experiment in seconds.
            file.write("%s\n" % (time))

            # The comment line.
            file.write("# %-18s %-20s %-20s\n" % ("nu_cpmg (Hz)", "R2eff (rad/s)", "Error"))

            # Loop over the spins of the cluster.
            for i in range(len(spins)):
                # Get the residue container.
                res = return_residue(spin_ids[i])

                # Name the residue if needed.
                res_name = res.name
                if res_name == None:
                    res_name = 'X'

                # Initialise the lines to output (to be able to catch missing data).
                lines = []

                # The residue ID line.
                lines.append("# %s%s\n" % (res_name, res.num))

                # Loop over the dispersion points.
                for offset, point in loop_offset_point(exp_type=exp_type, frq=frq, skip_ref=True):
                    # The parameter key.
                    param_key = return_param_key_from_data(exp_type=exp_type, frq=frq, offset=offset, point=point)

                    # No data.
                    if param_key not in spins[i].r2eff:
                        continue

                    # Store the data.
                    lines.append("%20.15g %20.13g %20.13g\n" % (point, spins[i].r2eff[param_key], spins[i].r2eff_err[param_key]))

                # No data.
                if len(lines) == 1:
                    continue

                # Write out the data.
                for line in lines:
                    file.write(line)

            # Close the file.
            file.close()

        # Increment the cluster index.
        cluster_index += 1


# -*- coding: utf-8 -*-
import numpy as np
import numpy.linalg as LA
import os
import argparse
import torch
import matplotlib.pyplot as plt
import random
import math
import glob
import skimage.io
import scipy.optimize
import sklearn.metrics
import scipy.sparse
from sklearn.metrics import pairwise_distances
from sklearn.cluster import DBSCAN
from vpd.models.sphere.sphere_utils import gold_spiral_sampling_patch

colours = ['#e6194b', '#4363d8', '#aaffc3', '#911eb4', '#46f0f0', '#f58231', '#3cb44b', '#f032e6',
           '#008080', '#bcf60c', '#fabebe', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3',
           '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000']

def single_eval_nyu(true_vps, estm_vps, separate_errors=True, normalised_coords=True, missing_vp_penalty=90.):

    ### camera intrinsics
    fx_rgb = 5.1885790117450188e+02
    fy_rgb = 5.1946961112127485e+02
    cx_rgb = 3.2558244941119034e+02
    cy_rgb = 2.5373616633400465e+02

    S = np.matrix([[1. / 320., 0, -1.], [0, 1. / 320., -.75], [0, 0, 1]])
    K = np.matrix([[fx_rgb, 0, cx_rgb], [0, fy_rgb, cy_rgb], [0, 0, 1]])
    SK = S * K
    Kinv = K.I
    SKinv = SK.I

    invmat = SKinv if normalised_coords else Kinv

    true_num_vps = true_vps.shape[0]
    true_vds = (invmat * np.matrix(true_vps).T).T
    for vi in range(true_vds.shape[0]):
        true_vds[vi,:] /= np.maximum(np.linalg.norm(true_vds[vi,:]), 1e-16)

    estm_num_vps = estm_vps.shape[0]
    num_vp_penalty = np.maximum(true_num_vps-estm_num_vps, 0)

    missing_vps = -estm_num_vps+true_num_vps

    estm_vds = (invmat * np.matrix(estm_vps).T).T
    for vi in range(estm_vds.shape[0]):
        estm_vds[vi,:] /= np.maximum(np.linalg.norm(estm_vds[vi,:]), 1e-16)

    cost_matrix = np.arccos(np.abs(np.array(true_vds * estm_vds.T))) * 180. / np.pi
    row_ind, col_ind = scipy.optimize.linear_sum_assignment(cost_matrix)
    loss = cost_matrix[row_ind, col_ind].sum() + num_vp_penalty * missing_vp_penalty

    errors = []
    for ri, ci in zip(row_ind, col_ind):
        errors += [cost_matrix[ri,ci]]
    if missing_vp_penalty > 0:
        errors += [missing_vp_penalty for _ in range(num_vp_penalty)]
    if separate_errors:
        return errors, missing_vps, row_ind, col_ind
    else:
        return loss, missing_vps, row_ind, col_ind

def calc_auc(error_array, cutoff=0.25):

    error_array = error_array.squeeze()
    error_array = np.sort(error_array)
    num_values = error_array.shape[0]

    plot_points = np.zeros((num_values, 2))

    midfraction = 1.

    for i in range(num_values):
        fraction = (i + 1) * 1.0 / num_values
        value = error_array[i]
        plot_points[i, 1] = fraction
        plot_points[i, 0] = value
        if i > 0:
            lastvalue = error_array[i - 1]
            if lastvalue < cutoff < value:
                midfraction = (lastvalue * plot_points[i - 1, 1] + value * fraction) / (value + lastvalue)

    if plot_points[-1, 0] < cutoff:
        plot_points = np.vstack([plot_points, np.array([cutoff, 1])])
    else:
        plot_points = np.vstack([plot_points, np.array([cutoff, midfraction])])

    sorting = np.argsort(plot_points[:, 0])
    plot_points = plot_points[sorting, :]

    auc = sklearn.metrics.auc(plot_points[plot_points[:, 0] <= cutoff, 0],
                              plot_points[plot_points[:, 0] <= cutoff, 1])
    auc = auc / cutoff

    return auc, plot_points

def vps_clustering(vps_prob, xyz, threshold):
    inds = np.flatnonzero(vps_prob >= threshold)
    vps = xyz[inds, :]
    dis = vps @ np.transpose(vps)
    dis = np.clip(dis, a_min=-1., a_max=1.)  ### same=1, opposite=-1, orthogonal=0
    dis = 1.0 - np.abs(dis)  ### same/opposite =0, orthogonal = 1

    dis_sparse = scipy.sparse.csr_matrix(dis)
    clusterer = DBSCAN(eps=0.005, min_samples=9, metric='precomputed').fit(dis_sparse)
    labels = clusterer.labels_
    # print('clusters', type(clusters), clusters.shape, np.unique(clusters))

    if labels.min()<=0: labels += (np.abs(labels.min())+1)  ### the labels from DBSCAN can be negtive (zeros) sometimes

    vps_pd=[]
    for label in np.unique(labels):
        inds_cluster = inds[labels==label]
        vp_max, vp_argmax = np.max(vps_prob[inds_cluster]), np.argmax(vps_prob[inds_cluster])
        vps_pd.append(np.array([inds_cluster[vp_argmax], vp_max]))
        # print('vps_pd', inds_cluster[vp_argmax], vp_max, len(inds_cluster))
    vps_pd = np.vstack(vps_pd)

    arg_prob = np.argsort(vps_pd[:, 1])[::-1]
    vps_pd_sort = vps_pd[arg_prob, 0].astype(int)

    # # # cluster labels for each spherical point
    vps_cluster = np.zeros(vps_prob.shape)
    vps_cluster[inds] = labels

    return xyz[vps_pd_sort], vps_cluster.astype(int)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description='NYU-VP dataset visualisation',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--data_dir', default="/tudelft.net/staff-bulk/ewi/insy/VisionLab/yanconglin/dataset/nyu_vp/processed_data", help='where to load')
    parser.add_argument('--pred_dir', default='/tudelft.net/staff-bulk/ewi/insy/VisionLab/yanconglin/vpd/VPS_code/logs/211103-130207-nyu/results_latest_nyu', help='where to save')
    parser.add_argument('--num_points', type=int, default=32768, help='number of spherical points')
    opt = parser.parse_args()

    xyz = gold_spiral_sampling_patch(np.array([0, 0, 1]), 90.0*np.pi/180.0, opt.num_points)

    imagelist = sorted(glob.glob(opt.data_dir + "/*_0.png"))[1224:] # test only
    filelist = sorted(glob.glob(opt.pred_dir + "/000???.npz")) # test only
    print('imagelist, filelist', len(imagelist), len(filelist))

    all_errors=[]
    for idx, (iname, file) in enumerate(zip(imagelist, filelist)):
        print('iname', idx, iname, file)
        image = skimage.io.imread(iname).astype(float)[:, :, :3]/255.0

        ############### load label ################################
        gtfile = np.load(iname.replace(".png", f".npz"), allow_pickle=True)
        vpts_gt = gtfile["vpts"]

       ############### load pd ################################
        predfile = np.load(file, allow_pickle=True)
        vpts_sphere = predfile["vpts_sphere"].astype(np.float32)
        vpts_pred, clusters = vps_clustering(vpts_sphere, xyz, threshold=0.5)
        vpts_pred = vpts_pred[0:len(vpts_gt)]  # topk

        errors, _, row_ind, col_ind = single_eval_nyu(vpts_gt, vpts_pred, missing_vp_penalty=90.)
        all_errors += errors

    np.savez_compressed(os.path.join(opt.pred_dir, 'error.npz'), error = np.hstack(all_errors))
    auc, plot_points = calc_auc(np.array(all_errors), cutoff=10)
    print("AUC: ", auc.shape, auc)
    plt.figure()
    plt.plot(plot_points[:, 0], plot_points[:, 1], 'b-', lw=3, label='AUC: %.3f ' % (auc * 100.))
    axes = plt.gca()
    axes.set_xlim([0, 10])
    axes.set_ylim([0, 1])
    plt.xlabel('error threshold', fontsize=14)
    plt.ylabel('recall', fontsize=14)
    plt.legend()
    plt.show()


import os
import json

class Store(object):
    _instances = {}
    data = {}
    def __new__(cls, path, *args, **kwargs):
        if path not in Store._instances:
            print("creating instance at", path)
            Store._instances[path] = super(Store, cls).__new__(cls, *args, **kwargs)
        return Store._instances[path]

    def __init__(self, path):
        if not self.data:
            self.dir = os.path.dirname(path)
            self.config_file = f"{path}.json"
            self.data = self.load()
            self.autosave = False

    def load(self):
        data = {}
        if os.path.isfile(self.config_file):
            print("Loading ", self.config_file)
            with open(self.config_file, "r") as f:
                data.update(json.load(f))
        else:
            print("no file", self.config_file)
        return data

    def save(self):
        if not os.path.isdir(self.dir):
            os.makedirs(self.dir, exist_ok=True)
        with open(self.config_file, 'w') as f:
            json.dump(self.data, f)

    def __getitem__(self, key):
        if key in self.data:
            return self.data[key]
        else:
            return None

    def __setitem__(self, key, value):
        self.data[key] = value
        if self.autosave:
            self.save()

    def __contains__(self, key):
        return self.data.__contains__(key)

    def __delitem__(self, key):
        del self.data[key]
        if sef.autosave:
            self.save()

def get_save(guild_id, name, sub_folder=None):
    if sub_folder:
        return Store(os.path.join("guilds", str(guild_id), sub_folder, name))
    return Store(os.path.join("guilds", str(guild_id), name))


try:
	from gc import collect

	def gcd(a, b):
		return (b==0 and a) or gcd(b, a%b)

	t = int(input())
	for i in range(t):
		n,l=[int(x) for x in input().split()]
		c=[int(x) for x in input().split()]
		p1=gcd(c[0], c[1])
		p2=c[0]//p1
		p=set()
		j=1
		while c[0]==c[j]: j+=1
		if (j%2==1 and c[j]%p2!=0) or (j%2==0 and c[j]%p1!=0): p1=p2
		p.add(p1)
		p2=p1
		for j in range(l):
			c[j]//=p2
			p2=c[j]
			p.add(p2)
		p=sorted(p)
		d={p[j]:chr(j+65) for j in range(26)}
		print("Case #{}: {}".format(i+1, d[p1]), end='')
		for j in c: print(d[j], end='')
		print()
		p=None
		d=None
		collect()
except e:
	pass