import random
from tools.find_reachable import find_reachable
# author: Vahid Ghafourian
# Date: 2023/09/06

from tools.find_path import find_path

class Kernel:
    def __init__(self, main_game, kernel_config) -> None:
        # initialize the player_id
        self.player_id = 0
        self.main_game = main_game
        self.kernel_config = kernel_config
        self.main_game.config = self.kernel_config


    def attack(self, attacking_id, target_id, fraction, move_fraction, player_id):
        # this API used to attack a node from another node

        # the body of the request should be like this
        ## attacking_id : the id of the node that will attack
        ## target_id : the id of the node that will be attacked
        ## fraction: the attack continues until the number of troops in the attacking node is fraction of the number of troops in the target node or the attacking node has only one troop or the target node has no troops
        ## move_fraction: the fraction of troops of attacking_id that will move to the target node after a successful attack

        # check if the game is in the turn state
        if self.main_game.game_state != 2:
            return {'error': 'The game is not in the turn state'}

        # check if the game is in the attack state
        if self.main_game.state != 2:
            return {'error': 'The game is not in the attack state'}

            # check if the attacking_id is valid
        if attacking_id not in self.main_game.nodes.keys():
            return {'error': 'attacking_id is not valid'}

        # check if the attacking_id has a owner
        if self.main_game.nodes[attacking_id].owner == None:
            return {'error': 'attacking_id does not have any owner'}

        # check if the attacking_id is owned by the player
        if self.main_game.nodes[attacking_id].owner.id != player_id:
            return {'error': 'attacking_id is not owned by the player'}

        # check if the target_id is valid
        if target_id not in self.main_game.nodes.keys():
            return {'error': 'target_id is not valid'}

        # check if the target_id has a owner
        if self.main_game.nodes[target_id].owner == None:
            return {'error': 'target_id does not have any owner'}

        # check if the target_id is not owned by the player
        if self.main_game.nodes[target_id].owner.id == player_id:
            return {'error': 'target_id is owned by the player'}
        try:
            fraction = float(fraction)
        except:
            return {'error': 'fraction is not valid it should be float'}

        try:
            move_fraction = float(move_fraction)
        except:
            return {'error': 'move_fraction is not valid it should be float'}

        # check if move fraction is between 0 and 1
        if move_fraction < 0 or move_fraction > 1:
            return {'error': 'move_fraction should be between 0 and 1'}

        # check if the player has at least 2 troops in the attacking node
        if self.main_game.nodes[attacking_id].number_of_troops < 2:
            return {'error': 'attacking node does not have enough troops'}

        # check if the fraction is a positive number
        if fraction < 0:
            return {'error': 'fraction should be positive'}

        # check if the attacker_id and target_id are connected
        if self.main_game.nodes[attacking_id] not in self.main_game.nodes[target_id].adj_main_map:
            return {'error': 'attacking_id and target_id are not connected'}

        attacker_troops = self.main_game.nodes[attacking_id].number_of_troops  # number of troops in the attacking node
        target_troops = self.main_game.nodes[target_id].number_of_troops + self.main_game.nodes[
            target_id].number_of_fort_troops  # number of troops in the target node

        # save the number of fort troops in the target node
        fort_troops = self.main_game.nodes[target_id].number_of_fort_troops
        normal_troops = self.main_game.nodes[target_id].number_of_troops

        while attacker_troops > 1 and target_troops > 0 and attacker_troops / target_troops > fraction:
            if attacker_troops > 3:
                attacker_dice = 3
            else:
                attacker_dice = attacker_troops - 1

            if target_troops >= 2:
                target_dice = 2
            else:
                target_dice = target_troops

            attacker_dice_list = []
            target_dice_list = []

            for _ in range(attacker_dice):
                attacker_dice_list.append(random.randint(1, 6))
            for _ in range(target_dice):
                target_dice_list.append(random.randint(1, 6))

            attacker_dice_list.sort(reverse=True)
            target_dice_list.sort(reverse=True)
            if self.main_game.config['debug_dice']:
                self.main_game.print(f'attacker troops: {attacker_troops} target troops: {target_troops}')
                self.main_game.print(f"attacker dice: {attacker_dice_list}" + f" target dice: {target_dice_list}")

            for i in range(min(attacker_dice, target_dice)):
                if attacker_dice_list[i] > target_dice_list[i]:
                    target_troops -= 1
                else:
                    attacker_troops -= 1
            if self.main_game.config['debug_dice']:
                self.main_game.print(f"new attacker troops: {attacker_troops}" + f" new target troops: {target_troops}")
                self.main_game.print(f'_________________________________________________________')

        # check if the attacker won
        if target_troops <= 0:
            move_troops = int(attacker_troops * move_fraction)
            if move_troops == 0:
                move_troops = 1

            if attacker_troops - move_troops < 1:
                move_troops -= 1
                # attacker_troops += 1

            self.main_game.nodes[attacking_id].number_of_troops = attacker_troops - move_troops
            self.main_game.nodes[target_id].number_of_troops = move_troops
            self.main_game.nodes[target_id].number_of_fort_troops = 0

            self.main_game.remove_node_from_player(target_id, self.main_game.nodes[target_id].owner.id)
            self.main_game.add_node_to_player(target_id, player_id)
            if self.main_game.has_won_troop == False:
                self.main_game.player_turn.number_of_troops_to_place += self.main_game.config[
                    'number_of_troops_after_successful_attack']
                self.main_game.has_won_troop = True

        else:
            if fort_troops > 0:
                if target_troops <= normal_troops:
                    self.main_game.nodes[target_id].number_of_fort_troops = 0
                    self.main_game.nodes[target_id].number_of_troops = target_troops
                else:
                    self.main_game.nodes[target_id].number_of_fort_troops = target_troops - normal_troops


            else:
                self.main_game.nodes[target_id].number_of_troops = target_troops

            self.main_game.nodes[attacking_id].number_of_troops = attacker_troops

        log = {
            "attacker": attacking_id,
            "target": target_id,
            "new_troop_count_attacker": self.main_game.nodes[attacking_id].number_of_troops,
            "new_troop_count_target": self.main_game.nodes[target_id].number_of_troops,
            "new_target_owner": self.main_game.nodes[target_id].owner.id,
            "new_fort_troop": self.main_game.nodes[target_id].number_of_fort_troops
        }
        self.main_game.log_attack.append(log)
        if self.main_game.debug:
            self.main_game.print(
                f"player {player_id} attacked node {target_id} from node {attacking_id} with fraction {fraction}. successful: {target_troops <= 0}")

        if target_troops <= 0:
            return {'message': 'attack successful', 'won': 1}
        else:
            return {'message': 'attack successful', 'won': 0}

    def fort(self, node_id, troop_count, player_id):
        # this API used to apply the fortification ability of the player

        # the body of the request should be like this
        ## node_id : the id of the node that will be fortified
        ## troop_count : the number of troops that will be fortified

        # check if the Game is in the turn state
        if self.main_game.game_state != 2:
            return {'error': 'The game is not in the turn state'}

        # check if the turn is in the fort state
        if self.main_game.state != 4:
            return {'error': 'The game is not in the fort state'}

        # check if the node_id is valid
        if node_id not in self.main_game.nodes.keys():
            return {'error': 'node_id is not valid'}

        # check the ownership status of the node
        # check if the node has an owner
        if self.main_game.nodes[node_id].owner is None:
            return {'error': 'This node has no owner'}

        # check if the node is owned by the player
        if self.main_game.nodes[node_id].owner.id != player_id:
            return {'error': 'This node is already owned by another player'}

        # check if the troop_count is valid
        if troop_count >= self.main_game.nodes[node_id].number_of_troops:
            return {'error': 'there is not enough troops in the node'}

        # check if the player hasn't used the fortification ability in the game
        if self.main_game.player_turn.use_fort:
            return {'error': 'you have already used the fortification ability in the game'}

        # start the fortification ability
        self.main_game.player_turn.use_fort = True

        # fortify the node
        self.main_game.nodes[node_id].number_of_troops -= troop_count
        self.main_game.nodes[node_id].number_of_fort_troops += self.main_game.config['fort_coef'] * troop_count

        if self.main_game.debug:
            self.main_game.print(f"player {player_id} fortified node {node_id} with {troop_count} troops")

        return {'success': 'the fortification ability is applied successfully'}

    def get_adj(self):
        # this API used to the list of the adjacent nodes of each node
        output_dict = {}
        for node in self.main_game.nodes.values():
            output_dict[node.id] = [i.id for i in node.adj_main_map]

        return output_dict

    def get_number_of_fort_troops(self):
        # this API used to get the number of fort troops on each node
        output_dict = {}
        for node in self.main_game.nodes.values():
            output_dict[node.id] = node.number_of_fort_troops
        return output_dict

    def get_number_of_troops_to_put(self):
        # return the number of troops that the player can put on the map
        output_dict = {"number_of_troops": self.main_game.player_turn.number_of_troops_to_place}
        return output_dict

    def get_owners(self, player_id):
        output_dict = {}
        for node in self.main_game.nodes.values():
            if node.owner != None:
                output_dict[node.id] = node.owner.id
            else:
                output_dict[node.id] = -1
        return output_dict

    def get_player_id(self, player_id):
        output_dict = {'player_id': player_id}
        return output_dict

    def get_reachable(self, node_id):
        # this API used to find all the nodes that the owner can move it's troops from node_id to them
        # body of the request should be like this:
        ## node_id: the id of the node that the player wants to move his troops from it

        # check if the node_id is valid
        if node_id not in self.main_game.nodes.keys():
            return {'error': 'node_id is not valid'}

        output_dict = {"reachable": find_reachable(node_id, self.main_game)}

        return output_dict

    def get_state(self, player_id):
        output_dict = {'state': self.main_game.state}
        return output_dict

    def get_strategic_nodes(self):
        output_dict = {'strategic_nodes': [i.id for i in self.main_game.nodes.values() if i.is_strategic],
                       'score': [i.score_of_strategic for i in self.main_game.nodes.values() if i.is_strategic]}
        return output_dict

    def get_troops_count(self, player_id):
        output_dict = {}
        for node in self.main_game.nodes.values():
            output_dict[node.id] = node.number_of_troops
        return output_dict

    def get_turn_number(self):
        output_dict = {'turn_number': self.main_game.turn_number}
        return output_dict

    def index(self):
        # this API used to check if the server is running
        return {"message": "Welcome, server is running"}

    def login(self):
        # make sure there is no more than number_of_players players
        if self.player_id >= self.kernel_config['number_of_players']:
            output_dict = {'error': 'game players is full'}
            raise output_dict['error']

        # create the output dictionary
        output_dict = {'player_id': self.player_id,
                       'message': 'login successful'}

        # initialize the player
        self.main_game.add_player(self.player_id)
        self.main_game.players[self.player_id].number_of_troops_to_place = self.main_game.config['initial_troop']
        self.player_id += 1
        return output_dict

    def move_troop(self, source, destination, troop_count, player_id):
        # this API used to move troops from source to destination

        # the body of the request should be like this
        ## source: the source node id
        ## destination: the destination node id
        ## troop_count: the number of troops to move

        # check if the move troop happened in the current turn
        if self.main_game.move_troop_done:
            return {'error': 'move troop already happened in the current turn'}

        # check if the game is in the turn state
        if self.main_game.game_state != 2:
            return {'error': 'The game is not in the turn state'}

        # check if the game is in the move troop state
        if self.main_game.state != 3:
            return {'error': 'The game is not in the move troop state'}

        # check if the source is valid
        if source not in self.main_game.nodes.keys():
            return {'error': 'source is not valid'}

        # check if the source has a owner
        if self.main_game.nodes[source].owner == None:
            return {'error': 'source does not have any owner'}

        # check if the source is owned by the player
        if self.main_game.nodes[source].owner.id != player_id:
            return {'error': 'source is not owned by the player'}

        # check if the destination is valid
        if destination not in self.main_game.nodes.keys():
            return {'error': 'destination is not valid'}

        # check if the destination has a owner
        if self.main_game.nodes[destination].owner == None:
            return {'error': 'destination does not have any owner'}

        if self.main_game.nodes[destination].owner.id != player_id:
            return {'error': 'destination is not owned by the player'}

        # check if the player has at least 2 troops in the source node
        if self.main_game.nodes[source].number_of_troops <= troop_count:
            return {'error': 'source node does not have enough troops'}

        # check if there is a path between source and destination
        res, path = find_path(source, destination, self.main_game, player_id)
        if not res:
            return {'error': 'there is no path between source and destination'}

        # check if the number of troops is positive
        if troop_count <= 0:
            return {'error': 'troop_count should be positive'}

        # check if the source and destination isn't same
        if source == destination:
            return {'error': 'source and destination should be different'}

        self.main_game.nodes[source].number_of_troops -= troop_count
        self.main_game.nodes[destination].number_of_troops += troop_count

        self.main_game.move_troop_done = True

        self.main_game.log_fortify = {"number_of_troops": troop_count,
                                      "path": path}

        if self.main_game.debug:
            self.main_game.print("player " + str(player_id) + " moved " + str(troop_count) + " troops from node " + str(
                source) + " to node " + str(destination))

        return {'message': 'troops moved successfully'}

    def next_state(self, player_id):
        '''
        This function is used to change the state of the game to the next state
        1: put troop state
        2: attack state
        3: move troop state
        4: fortification state
        '''
        if self.main_game.game_state != 2:
            output_dict = {'error': 'The game is not in the turn state'}
            return output_dict

        if self.main_game.state >= 4:
            output_dict = {'error': 'you already finished the turn'}
            return output_dict

        self.main_game.state += 1
        if self.main_game.debug:
            self.main_game.print("******* state changed to: " + str(self.main_game.state) + " *******")

        output_dict = {'game_state': self.main_game.state, 'message': 'success'}
        return output_dict

    def put_one_troop(self, player_id, node_id):
        # this API is used to put one troop on the map in the initial troop state of the game

        # body of the request should be like this:
        ## node_id: the id of the node that the player wants to put the troop on it

        # check if the player just put one Troop in a init turn
        if self.main_game.state != 1:
            return {'error': 'You can not put more than one troop in a turn'}
        # check if the game is in the initial troop putting state
        if self.main_game.game_state != 1:
            return {'error': 'The game is not in the initial troop putting state'}

        # check if the player has enough troops to put
        if self.main_game.player_turn.number_of_troops_to_place <= 0:
            return {'error': 'You have no more initial troops to put'}

        # get the node_id from the request body
        node_id = node_id

        # check if the node_id is valid
        if node_id not in self.main_game.nodes.keys():
            return {'error': 'node_id is not valid'}

        # check the ownership status of the node
        if self.main_game.nodes[node_id].owner is None:
            # if the node is not owned by any player, add it to the player
            self.main_game.add_node_to_player(node_id, player_id)

        elif self.main_game.nodes[node_id].owner.id != player_id:
            return {'error': 'This node is already owned by another player'}

        # add one troop to the node and subtract one from the player
        self.main_game.nodes[node_id].number_of_troops += 1
        self.main_game.player_turn.number_of_troops_to_place -= 1

        # add the node id and player id to the log variable of the game
        self.main_game.log_initialize.append([player_id, node_id])

        # change the state to 2 so player just can put one troop in a turn
        self.main_game.state = 4
        if self.main_game.debug:
            self.main_game.print(f"player {player_id} put one troop on node {node_id}")

        return {'message': 'troop added successfully'}

    def put_troop(self, player_id, node_id, number_of_troops):
        # this API used to put troops in the map in the put troop state

        # body of the request should be like this:
        ## node_id: the id of the node that the player wants to put the troop on it
        ## number_of_troops: the number of troops that the player wants to put on the node

        # check if the game is in the turn state
        if self.main_game.game_state != 2:
            return {'error': 'The game is not in the turn state'}

        # check if the turn in the put troop state
        if self.main_game.state != 1:
            return {'error': 'The game is not in the troop putting state'}

        # check if the player has enough troops to place
        if self.main_game.player_turn.number_of_troops_to_place < number_of_troops:
            return {'error': 'You do not have enough troops to place'}

        # check if the node is not owned by anyone
        if self.main_game.nodes[node_id].owner is None:
            self.main_game.add_node_to_player(node_id, player_id)

        # check if the node is not owned by another player
        elif self.main_game.nodes[node_id].owner.id != player_id:
            return {'error': 'This node is already owned by another player'}

        # check if the number_of_troops is positive
        if number_of_troops <= 0:
            return {'error': 'number_of_troops should be positive'}

        # add one troop to the node and subtract one from the player
        self.main_game.nodes[node_id].number_of_troops += number_of_troops
        self.main_game.player_turn.number_of_troops_to_place -= number_of_troops

        # add the node id and player id to the log variable of the game
        self.main_game.log_put_troop.append([node_id, number_of_troops])

        if self.main_game.debug:
            self.main_game.print(
                "player " + str(player_id) + " put " + str(number_of_troops) + " troops on node " + str(node_id))

        return {'message': 'troop added successfully'}

    """
    in this API client shows that it's ready to start the game 
    that means it has a server on the port that it said in the login API
    """

    def ready(self, player_id):
        # try:
        self.main_game.players[player_id].is_ready = True
        output_dict = {"message": "every thing is ok, you should wait for other players to be ready"}
        self.main_game.check_all_players_ready()
        return output_dict

        # except:
        output_dict = {"error": "this player_id doesn't exist"}
        return output_dict
Kernel.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06

from clients.game_client import GameClient
import random

from clients.stages.attack import plan_attack
from clients.stages.initializer import initialize_turn


class ClientAi:
    def __init__(self, kernel, name_id=0) -> None:
        self.flag = False
        self.kernel = kernel
        self.game = self.join_kernel()
        self.game.ai_client = self
        self.name_id = name_id
        print(self.kernel.ready(self.game.get_player_id()))

    def join_kernel(self):
        login_response = self.kernel.login()
        id = login_response['player_id']
        # generate game object
        game = GameClient(self.kernel, id)
        return game

    def initializer_turn(self):
        print('start')
        initialize_turn(self.game)
        self.game.next_state()

    def turn(self):
        print('start attack')
        print(self.game.game_data.player_id)
        self.game.game_data.stage = 1
        plan_attack(self.game)
        self.game.game_data.phase_2_turns += 1
        self.game.next_state()

    def get_game(self):
        return self.game

    def __name__(self):
        return f"Aggressive Chad {self.name_id}"

clients/client_ai.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06

from clients.game_client import GameClient
import random


class ClientEnemyOne():
    def __init__(self, kernel) -> None:
        self.flag = False
        self.kernel = kernel
        self.game = self.join_kernel()
        print(self.kernel.ready(self.game.get_player_id()))

    def join_kernel(self):
        login_response = self.kernel.login()
        id = login_response['player_id']
        # generate game object
        game = GameClient(self.kernel, id)
        return game

    def initializer_turn(self):
        strategic_nodes = self.game.get_strategic_nodes()['strategic_nodes']
        score = self.game.get_strategic_nodes()['score']
        strategic_nodes = list(zip(strategic_nodes, score))
        strategic_nodes.sort(key=lambda x: x[1], reverse=True)
        strategic_nodes, score = list(zip(*strategic_nodes))

        owner = self.game.get_owners()
        for i in strategic_nodes:
            if owner[i] == -1:
                print(self.game.put_one_troop(i), "-- putting one troop on", i)
                return
        '''
        adj = self.game.get_adj()
        for i in strategic_nodes:
            for j in adj[i]:
                if owner[j] == -1:
                    print(self.game.put_one_troop(j), "-- putting one troop on neighbor of strategic node", j)
                    return
        my_id = self.game.get_player_id()
        nodes = []
        nodes.extend([i for i in strategic_nodes if owner[i] == my_id])
        for i in strategic_nodes:
            for j in adj[i]:
                if owner[j] == my_id:
                    nodes.append(j)
        nodes = list(set(nodes))
        node = random.choice(nodes)
        self.game.put_one_troop(node)
        print("3-  putting one troop on", node)'''

    def turn(self):
        '''
        print(self.game.get_number_of_troops_to_put())
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == -1 and self.game.get_number_of_troops_to_put()['number_of_troops'] > 1:
                print(self.game.put_troop(i, 1))

        list_of_my_nodes = []
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                list_of_my_nodes.append(i)

        print(self.game.put_troop(random.choice(list_of_my_nodes),
                                  self.game.get_number_of_troops_to_put()['number_of_troops']))
        print(self.game.get_number_of_troops_to_put())

        print(self.game.next_state())

        # find the node with the most troops that I own
        max_troops = 0
        max_node = -1
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                if self.game.get_number_of_troops()[i] > max_troops:
                    max_troops = self.game.get_number_of_troops()[i]
                    max_node = i
        # find a neighbor of that node that I don't own
        adj = self.game.get_adj()
        for i in adj[max_node]:
            if owner[i] != self.game.get_player_id() and owner[i] != -1:
                print(self.game.attack(max_node, i, 1, 0.5))
                break
        print(self.game.next_state())
        print(self.game.get_state())
        # get the node with the most troops that I own
        max_troops = 0
        max_node = -1
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                if self.game.get_number_of_troops()[i] > max_troops:
                    max_troops = self.game.get_number_of_troops()[i]
                    max_node = i
        print(self.game.get_reachable(max_node))
        destination = random.choice(self.game.get_reachable(max_node)['reachable'])
        print(self.game.move_troop(max_node, destination, 1))
        print(self.game.next_state())

        if self.flag == False:
            max_troops = 0
            max_node = -1
            owner = self.game.get_owners()
            for i in owner.keys():
                if owner[i] == self.game.get_player_id():
                    if self.game.get_number_of_troops()[i] > max_troops:
                        max_troops = self.game.get_number_of_troops()[i]
                        max_node = i

            print('Max Number of node in best planet', self.game.get_number_of_troops()[max_node])
            print(self.game.fort(max_node, 3))
            print(self.game.get_number_of_fort_troops())
            self.flag = True
        '''
clients/client_enemy_one.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06

from clients.game_client import GameClient
import random


class ClientEnemyTwo():
    def __init__(self, kernel, name_id=0) -> None:
        self.flag = False
        self.kernel = kernel
        self.game = self.join_kernel()
        self.name_id = name_id
        print(self.kernel.ready(self.game.get_player_id()))

    def join_kernel(self):
        login_response = self.kernel.login()
        id = login_response['player_id']
        # generate game object
        game = GameClient(self.kernel, id)
        return game

    def initializer_turn(self):
        strategic_nodes = self.game.get_strategic_nodes()['strategic_nodes']
        score = self.game.get_strategic_nodes()['score']
        strategic_nodes = list(zip(strategic_nodes, score))
        strategic_nodes.sort(key=lambda x: x[1], reverse=True)
        strategic_nodes, score = list(zip(*strategic_nodes))

        owner = self.game.get_owners()
        for i in strategic_nodes:
            if owner[i] == -1:
                print(self.game.put_one_troop(i), "-- putting one troop on", i)
                return
        adj = self.game.get_adj()
        for i in strategic_nodes:
            for j in adj[i]:
                if owner[j] == -1:
                    print(self.game.put_one_troop(j), "-- putting one troop on neighbor of strategic node", j)
                    return
        my_id = self.game.get_player_id()
        nodes = []
        nodes.extend([i for i in strategic_nodes if owner[i] == my_id])
        for i in strategic_nodes:
            for j in adj[i]:
                if owner[j] == my_id:
                    nodes.append(j)
        nodes = list(set(nodes))
        node = random.choice(nodes)
        self.game.put_one_troop(node)
        print("3-  putting one troop on", node)

    def turn(self):
        print(self.game.get_number_of_troops_to_put())
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == -1 and self.game.get_number_of_troops_to_put()['number_of_troops'] > 1:
                print(self.game.put_troop(i, 1))

        list_of_my_nodes = []
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                list_of_my_nodes.append(i)

        print(self.game.put_troop(random.choice(list_of_my_nodes),
                                  self.game.get_number_of_troops_to_put()['number_of_troops']))
        print(self.game.get_number_of_troops_to_put())

        print(self.game.next_state())

        # find the node with the most troops that I own
        max_troops = 0
        max_node = -1
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                if self.game.get_number_of_troops()[i] > max_troops:
                    max_troops = self.game.get_number_of_troops()[i]
                    max_node = i
        # find a neighbor of that node that I don't own
        adj = self.game.get_adj()
        for i in adj[max_node]:
            if owner[i] != self.game.get_player_id() and owner[i] != -1:
                print(self.game.attack(max_node, i, 1, 0.5))
                break
        print(self.game.next_state())
        print(self.game.get_state())
        # get the node with the most troops that I own
        max_troops = 0
        max_node = -1
        owner = self.game.get_owners()
        for i in owner.keys():
            if owner[i] == self.game.get_player_id():
                if self.game.get_number_of_troops()[i] > max_troops:
                    max_troops = self.game.get_number_of_troops()[i]
                    max_node = i
        print(self.game.get_reachable(max_node))
        destination = random.choice(self.game.get_reachable(max_node)['reachable'])
        print(self.game.move_troop(max_node, destination, 1))
        print(self.game.next_state())

        if self.flag == False:
            max_troops = 0
            max_node = -1
            owner = self.game.get_owners()
            for i in owner.keys():
                if owner[i] == self.game.get_player_id():
                    if self.game.get_number_of_troops()[i] > max_troops:
                        max_troops = self.game.get_number_of_troops()[i]
                        max_node = i

            print('Max Number of node in best planet', self.game.get_number_of_troops()[max_node])
            print(self.game.fort(max_node, 3))
            print(self.game.get_number_of_fort_troops())
            self.flag = True

    def __name__(self):
        return f"Random AF {self.name_id}"

clients/client_enemy_two.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06
from clients.stages.initializer import initialize_turn
import json
import online_src
from clients.game_client import GameClient
from clients.strategy.mess_strategy import MessStrategy
from clients.strategy.one_surprise_attack import OneSurpriseAttack
from clients.strategy.plus3_strategy import Plus3Strategy
from clients.strategy.two_surprise_attack import TwoSurpriseAttack
from clients.utils.maximize_score import maximize_score
from clients.utils.update_details import GameData

game_config = json.load(open('config.json', 'r'))


def attack_function(game: GameClient | online_src.game.Game):
    gdata: GameData = game.game_data
    gdata.update_game_state()
    my_strategic = [node for node in gdata.nodes if node.owner == gdata.player_id and node.is_strategic]

    max_strategics = -1
    for pi in range(gdata.player_cnt):
        if pi == gdata.player_id:
            continue
        num_stra = len([n for n in gdata.nodes if n.owner == pi])
        if max_strategics < num_stra:
            max_strategics = num_stra
    # surprise two strategic attack
    condition = (
                        gdata.turn_number >= 126 and len(my_strategic) >= 2
                ) or (gdata.turn_number == 125 and max_strategics >= 5)
    two_surprise_attack_strategy = TwoSurpriseAttack(game, condition)
    shall_pass = two_surprise_attack_strategy.compute_plan()
    if shall_pass:
        two_surprise_attack_strategy.run_strategy()
        return

    # "get much as territory when last player in last round" strategy
    is_last_turn = (game_config['number_of_turns'] - game.get_turn_number()['turn_number'] < 3)
    if (is_last_turn and gdata.player_id == 0) or (gdata.turn_number == 126 and len(my_strategic) >= 4):
        maximize_score(game)
        game.next_state()
        # no move troops
        game.next_state()
        # no fort
        return

    # surprise one strategic attack
    one_surprise_attack_strategy = OneSurpriseAttack(game)
    shall_pass = one_surprise_attack_strategy.compute_plan()
    if shall_pass:
        one_surprise_attack_strategy.run_strategy()
        return

    # mess with the opposition
    mess_strategy = MessStrategy(game)
    shall_pass = mess_strategy.compute_plan()
    if shall_pass:
        mess_strategy.run_strategy()
        return

    # +3 force attack
    plus3_strategy = Plus3Strategy(game)
    plus3_strategy.run_strategy_until_success()


class ClientCustom:
    def __init__(self, kernel, name_id=0) -> None:
        self.flag = False
        self.kernel = kernel
        self.game = self.join_kernel()
        self.game.ai_client = self
        self.name_id = name_id
        print(self.kernel.ready(self.game.get_player_id()))

    def join_kernel(self):
        login_response = self.kernel.login()
        id = login_response['player_id']
        # generate game object
        game = GameClient(self.kernel, id)
        return game

    def initializer_turn(self):
        print('start')
        initialize_turn(self.game)
        self.game.next_state()

    def turn(self):
        print('start attack')
        print(self.game.game_data.player_id)
        self.game.game_data.stage = 1
        attack_function(self.game)
        self.game.game_data.phase_2_turns += 1
        self.game.next_state()

    def get_game(self):
        return self.game

    def __name__(self):
        return f"Custom Chad {self.name_id}"

clients/custom_client.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06

class GameClient:
    def __init__(self, kernel, player_id=0):
        self.kernel = kernel
        self.my_turn = False
        self.player_id = player_id
        from clients.utils.update_details import GameData
        self.game_data: GameData = GameData(self)

    def get_owners(self):
        """
            returns a dictionary of node_id: owner_id
            node_id: str
            owner_id: int
        """
        return self.kernel.get_owners(self.player_id)

    def get_number_of_troops(self):
        """
            returns a dictionary of node_id: number_of_troops
            node_id: str
            number_of_troops: int
        """
        return self.kernel.get_troops_count(self.player_id)

    def get_state(self):
        """
            returns a dictionary containing the state of the game
            1: put_troop
            2: attack
            3: move_troop
            4: fort
            {'state': number_of_state}
        """
        return self.kernel.get_state(self.player_id)

    def get_turn_number(self):
        """
            returns a dictionary containing the turn number
            {'turn_number': number_of_turn}
        """
        return self.kernel.get_turn_number()

    def get_adj(self):
        """
            return the adjacent nodes of each node
            returns a dictionary of node_id: [adjacent_nodes]
            node_id: str
            adjacent_nodes: list of int
        """
        return self.kernel.get_adj()

    def next_state(self):
        """
            changes the state of the turn to the next state
        """
        return self.kernel.next_state(self.player_id)

    def put_one_troop(self, node_id):
        """
            puts one troop in the node with the given id
            this function can only be used in the put_troop state in the initialize function
        """
        return self.kernel.put_one_troop(self.player_id, node_id)

    def put_troop(self, node_id, num):
        """
            puts num troops in the node with the given id
            this function can only be used in the put_troop state in the turn function
        """
        return self.kernel.put_troop(self.player_id, node_id, num)

    def get_player_id(self):
        """
            returns the id of the player
        """
        return self.player_id

    def attack(self, attacking_id, target_id, fraction, move_fraction):
        """
            attacks the target node with the given fraction of troops
        """
        return self.kernel.attack(attacking_id, target_id, fraction, move_fraction, self.player_id)

    def move_troop(self, source, destination, troop_count):
        """
            moves the given number of troops from the source node to the destination node
        """
        return self.kernel.move_troop(source, destination, troop_count, self.player_id)

    def get_strategic_nodes(self):
        """
            returns a list of strategic nodes and their score
            {"strategic_nodes": [node_id, ...], "score": [score, ...]}
        """
        return self.kernel.get_strategic_nodes()

    def get_number_of_troops_to_put(self):
        """
            returns the number of troops that the player can put in the put_troop state
            {"number_of_troops": number_of_troops}
        """
        return self.kernel.get_number_of_troops_to_put()

    def get_reachable(self, node_id):
        """
            returns a dictionary of "reachable" key and a list of reachable nodes
            {"reachable": [node_id, ...]}
        """
        return self.kernel.get_reachable(node_id)

    def get_number_of_fort_troops(self):
        """
            returns the number of troops that used to defend the node
            {node_id: number_of_troops, ...}
            node_id: str
            number_of_troops: int
        """
        return self.kernel.get_number_of_fort_troops()

    def fort(self, node_id, troop_count):
        """
            fortifies the node with the given number of troops
        """
        return self.kernel.fort(node_id, troop_count, self.player_id)

clients/game_client.py
AICUP2023-Hybrid/Agent
import math

from clients.game_client import GameClient
import random

from clients.stages.attack import plan_attack
from clients.stages.initializer import initialize_turn
from clients.utils.get_possible_danger import get_node_danger


class ClientSalar:

    def __init__(self, kernel, name_id=0) -> None:
        self.flag = False
        self.kernel = kernel
        self.game = self.join_kernel()
        self.non_strategic_node = 0
        self.name_id = name_id
        print(self.kernel.ready(self.game.get_player_id()))

    def join_kernel(self):
        login_response = self.kernel.login()
        id = login_response['player_id']
        # generate game object
        game = GameClient(self.kernel, id)
        return game

    def initializer_turn(self):
        self.game.game_data.update_game_state()

        gdata = self.game.game_data

        # Get the strategic resource
        nodes_sorted = sorted(gdata.nodes, key=lambda x: -x.score_of_strategic)
        for node in nodes_sorted:
            if not node.is_strategic:
                break
            if node.owner is None:
                print(self.game.put_one_troop(node.id), "-- putting one troop on", node.id)
                return

        my_nodes = [
            (node, get_node_danger(self.game.game_data, node))
            for node in nodes_sorted if node.owner == gdata.player_id and node.is_strategic
        ]
        my_nodes = sorted(my_nodes, key=lambda x: -x[1])
        print([(node.id, node.number_of_troops, danger) for node, danger in my_nodes])
        print('complex calculation')
        for node, danger in my_nodes:
            if danger > 0:
                print(self.game.put_one_troop(node.id), "-- putting one troop on", node.id)
                return

        if self.non_strategic_node >= 0:
            return

        for node in nodes_sorted:
            if node.is_strategic:
                continue
            if node.owner is None and get_node_danger(self.game.game_data, node) <= 0:
                print(self.game.put_one_troop(node.id), "-- putting one troop on", node.id)
                return

    def turn(self):
        print('start attack')
        print(self.game.game_data.player_id)
        self.game.game_data.update_game_state()

        gdata = self.game.game_data

        if self.game.player_id != 1:
            # Get the strategic resource
            my_nodes = [
                (node, get_node_danger(gdata, node))
                for node in gdata.nodes if node.owner == gdata.player_id and node.is_strategic
            ]
            my_nodes = sorted(my_nodes, key=lambda x: -x[1])

            for node, danger in my_nodes:
                if danger > 0:
                    troops = min(math.ceil(danger / 2), gdata.remaining_init[gdata.player_id])
                    print(self.game.put_troop(node.id, troops), f"-- putting {troops} troop on {node.id}")
                    break
            plan_attack(self.game, should_fort=False)

            my_nodes = [
                (node, get_node_danger(gdata, node))
                for node in gdata.nodes if node.owner == gdata.player_id and node.is_strategic
            ]
            my_nodes = sorted(my_nodes, key=lambda x: -x[1])
            for node, danger in my_nodes:
                if danger > 0:
                    troops = min(math.ceil(danger / 2), node.number_of_troops)
                    print(self.game.fort(node.id, troops), f"-- forting {troops} troop on {node.id}")
                    break
        else:
            plan_attack(self.game, should_fort=True)

        self.game.game_data.phase_2_turns += 1

    def get_game(self):
        return self.game

    def __name__(self):
        return f"Madar khoda Chad {self.name_id}"

clients/salar/SalarAgent.py
AICUP2023-Hybrid/Agent
import json
import online_src
from clients.game_client import GameClient
from clients.strategy.mess_strategy import MessStrategy
from clients.strategy.one_surprise_attack import OneSurpriseAttack
from clients.strategy.plus3_strategy import Plus3Strategy
from clients.strategy.two_surprise_attack import TwoSurpriseAttack
from clients.utils.maximize_score import maximize_score
from clients.utils.update_details import GameData
game_config = json.load(open('config.json', 'r'))


def plan_attack(game: GameClient | online_src.game.Game):
    gdata: GameData = game.game_data
    gdata.update_game_state()
    my_strategic = [node for node in gdata.nodes if node.owner == gdata.player_id and node.is_strategic]

    max_strategics = -1
    for pi in range(gdata.player_cnt):
        if pi == gdata.player_id:
            continue
        num_stra = len([n for n in gdata.nodes if n.owner == pi])
        if max_strategics < num_stra:
            max_strategics = num_stra
    # surprise two strategic attack
    condition = (
            gdata.turn_number >= 126 and len(my_strategic) == 2
    ) or (gdata.turn_number == 125 and max_strategics >= 5)
    two_surprise_attack_strategy = TwoSurpriseAttack(game, condition)
    shall_pass = two_surprise_attack_strategy.compute_plan()
    if shall_pass:
        two_surprise_attack_strategy.run_strategy()
        return

    # "get much as territory when last player in last round" strategy
    is_last_turn = (game_config['number_of_turns'] - game.get_turn_number()['turn_number'] < 3)
    if (is_last_turn and gdata.player_id == 0) or (gdata.turn_number == 126 and len(my_strategic) >= 4):
        maximize_score(game)
        game.next_state()
        # no move troops
        game.next_state()
        # no fort
        return

    # surprise one strategic attack
    one_surprise_attack_strategy = OneSurpriseAttack(game)
    shall_pass = one_surprise_attack_strategy.compute_plan()
    if shall_pass:
        one_surprise_attack_strategy.run_strategy()
        return

    # mess with the opposition
    # mess_strategy = MessStrategy(game)
    # shall_pass = mess_strategy.compute_plan()
    # if shall_pass:
    #     mess_strategy.run_strategy()
    #     return

    # +3 force attack
    plus3_strategy = Plus3Strategy(game)
    plus3_strategy.run_strategy_until_success()

clients/stages/attack.py
AICUP2023-Hybrid/Agent
from math import ceil

import networkx as nx
import numpy as np

import online_src
from clients.game_client import GameClient
from clients.utils.attack_chance import get_expected_casualty
from clients.utils.get_possible_danger import get_node_danger
from clients.utils.update_details import GameData
from components.node import Node


def get_init_score(gdata: GameData, nodes):
    graph = gdata.get_board_graph()
    strategics = [[node, 0] for node in gdata.nodes if node.is_strategic and node.owner != gdata.player_id]
    for src in nodes:
        too_close = len([nei for nei in src.adj_main_map if nei.is_strategic]) > 0
        if too_close:
            continue
        lengths = nx.shortest_path_length(graph, source=src.id)
        for i, (node, cnt) in enumerate(strategics):
            if node.id in lengths and lengths[node.id] <= 3:
                point = [0, 0, 1, 0.25]
                strategics[i][1] += point[lengths[node.id]]
    dist = 0
    for a in nodes:
        for b in nodes:
            dist += nx.shortest_path_length(graph, source=a.id, target=b.id)
    score = 0
    for node, cnt in strategics:
        score += max(cnt, 1)
    return score + 0.01 * dist


def initialize_turn(game: GameClient | online_src.game.Game):
    game.game_data.update_game_state()

    gdata = game.game_data

    # Get the strategic resource
    nodes_sorted = sorted(gdata.nodes, key=lambda x: -x.score_of_strategic)
    for node in nodes_sorted:
        if not node.is_strategic:
            break
        if node.owner is None:
            print(game.put_one_troop(node.id), "-- putting one troop on", node.id)
            return

    # Add two nodes to initialization for reachability and extra troop
    my_nodes = [node for node in gdata.nodes if node.owner == gdata.player_id]
    if len(my_nodes) < 4:
        max_score, tar = -np.Inf, None
        for mid1 in gdata.nodes:
            if mid1.owner is not None:
                continue
            score = get_init_score(gdata, my_nodes + [mid1])
            if max_score < score:
                max_score = score
                tar = mid1
            if len(my_nodes) < 3:
                for mid2 in gdata.nodes:
                    if mid1 == mid2 or mid2.owner is not None:
                        continue
                    score = get_init_score(gdata, my_nodes + [mid1, mid2])
                    if max_score < score:
                        max_score = score
                        tar = mid1
        if tar is not None:
            print(game.put_one_troop(tar.id), "-- putting one troop on", tar.id)
            return

    my_nodes = [
        (
            node,
            node.score_of_strategic,
            get_node_danger(
                game.game_data,
                node
            )
        )
        for node in nodes_sorted if node.owner == gdata.player_id and node.is_strategic
    ]
    dont_put = 20
    if gdata.remaining_init[gdata.player_id] < dont_put:
        return
    if ceil((125 - gdata.turn_number + 1) / 3) <= 35 - dont_put:
        my_nodes = sorted(my_nodes, key=lambda x: -x[1])
        if my_nodes[-1][0].score_of_strategic == 1:
            my_nodes.pop()
        for node, score, danger in my_nodes:
            if danger > 0:
                print(game.put_one_troop(node.id), "-- putting one troop on", node.id)
                return

clients/stages/initializer.py
AICUP2023-Hybrid/Agent
import numpy as np

from clients.strategy.startegy import *
from clients.strategy.utils.balance_two_strategic import balance_troops_between_two_strategics
from clients.strategy.utils.path_attack_sequence import get_one_path_attack_sequence
from clients.utils.attack_chance import get_expected_casualty
from clients.utils.get_possible_danger import get_surprise_danger, get_min_loss_path


class MessStrategy(Strategy):
    def __init__(self, game: GameClient | Game):
        super().__init__(game)
        self.troops_to_put = 0
        self.attack_path = None

    def put_troops(self) -> List[PutTroopsAction]:
        return [PutTroopsAction(node=self.attack_path[0], number_of_troops=self.troops_to_put)]

    def attacks(self) -> List[AttackAction]:
        return get_one_path_attack_sequence(self.attack_path)

    def move_troop(self) -> Optional[MoveTroopAction]:
        gdata = self.game.game_data
        attack_path = self.attack_path
        if attack_path[0].is_strategic and attack_path[-1].owner == gdata.player_id:
            return balance_troops_between_two_strategics(gdata, attack_path[-1], attack_path[0])

    def fortify(self) -> Optional[FortAction]:
        return None

    def compute_plan(self,attempt=0):
        gdata = self.game.game_data

        remaining_troops = gdata.remaining_init[gdata.player_id]
        opposing_players = [i for i in range(gdata.player_cnt) if i != gdata.player_id]

        final_troops_to_put, target_n_strategics, target_troop_gain, final_path = 0, 0, 0, []
        for player in opposing_players:
            strategics = [node for node in gdata.nodes if node.owner == player and node.is_strategic]
            player_troop_gain = 0
            for node in strategics:
                player_troop_gain += node.score_of_strategic
            player_troop_gain += len([node for node in gdata.nodes if node.owner == player]) // 4
            n_strategics = len(strategics)

            troops_to_put, best_score, best_path = 0, 0, []
            attack_power_threshold = 2
            for st_node in strategics:
                final_to_place, loss, path = 0, np.Inf, []
                for to_place in range(remaining_troops + 1):
                    loss, path = get_min_loss_path(
                        gdata, st_node, gdata.player_id,
                        max_troops_to_put=to_place,
                        attack_power_threshold=attack_power_threshold
                    )
                    final_to_place = to_place
                    # threshold
                    if len(path) > 0:
                        break
                if len(path) == 0:
                    continue

                # +3 for troops gain after attack
                p_gain = st_node.score_of_strategic * 0.75
                score = p_gain - loss + 3 - attack_power_threshold
                if best_score <= score:
                    troops_to_put = final_to_place
                    best_score = score
                    best_path = path

            if n_strategics > target_n_strategics or (
                    n_strategics == target_n_strategics and player_troop_gain > target_troop_gain
            ):
                final_troops_to_put = troops_to_put
                target_n_strategics = n_strategics
                target_troop_gain = player_troop_gain
                final_path = best_path

        self.attack_path = final_path
        self.troops_to_put = final_troops_to_put
        return len(final_path) > 0

clients/strategy/mess_strategy.py
AICUP2023-Hybrid/Agent
import math

import networkx as nx
import numpy as np

from clients.strategy.startegy import *
from clients.strategy.utils.balance_two_strategic import balance_troops_between_two_strategics, \
    check_balance_two_strategic_possible
from clients.strategy.utils.choose_fort_nodes import get_fort_from_nodes
from clients.utils.algorithms import binary_search, balance_two_nodes_binary_search
from clients.utils.attack_chance import get_expected_casualty, get_win_rate, get_attack_outcomes, MAX_TROOP_CALC

from clients.utils.get_possible_danger import get_surprise_danger, get_node_danger


class OneSurpriseAttack(Strategy):
    def __init__(self, game: GameClient | Game):
        super().__init__(game)
        self.max_loss_cache = dict()
        self.can_move = None
        self.can_fort = None
        self.troops_to_put = 0
        self.attack_path = None

    def put_troops(self) -> List[PutTroopsAction]:
        if len(self.attack_path) == 1:
            gdata = self.game.game_data
            src = self.attack_path[0]
            src_one_troop_danger = get_node_danger(gdata, src)

            def can_save_node(remaining_troops: int, save_node: Node, danger_on_one_troop):
                if remaining_troops == 0:
                    return False
                exp = get_expected_casualty()
                initial_troops = save_node.number_of_troops
                save_node.save_version()
                save_node.number_of_troops += remaining_troops
                if self.can_fort and not gdata.done_fort:
                    save_node.number_of_troops *= 2
                    save_node.number_of_troops -= 1
                d = danger_on_one_troop + exp[initial_troops + save_node.number_of_fort_troops]
                d -= exp[save_node.number_of_fort_troops + save_node.number_of_troops]
                save_node.restore_version()
                return d <= 0

            save_src_troops = binary_search(
                0, self.troops_to_put, can_save_node,
                False, save_node=src, danger_on_one_troop=src_one_troop_danger
            )
            if save_src_troops > 0:
                put = min(self.troops_to_put, save_src_troops + 2)
                return [PutTroopsAction(node=self.attack_path[0], number_of_troops=put)]
        else:
            return [PutTroopsAction(node=self.attack_path[0], number_of_troops=self.troops_to_put)]

    def attacks(self) -> List[AttackAction]:
        path = self.attack_path
        attack_actions = []
        for i in range(len(path) - 1):
            attack = AttackAction(src=path[i], dest=path[i + 1], fraction=0, move_fraction=0.999)
            attack_actions.append(attack)
        return attack_actions

    def move_troop(self) -> Optional[MoveTroopAction]:
        if not self.can_move:
            return None
        gdata = self.game.game_data
        max_path = self.attack_path
        if max_path[0].is_strategic and max_path[-1].owner == gdata.player_id and len(max_path) > 1:
            if not max_path[-1].is_strategic and max_path[-1].number_of_troops > 1:
                return MoveTroopAction(
                    src=max_path[-1], dest=max_path[0], count=max_path[-1].number_of_troops - 1
                )
            return balance_troops_between_two_strategics(gdata, max_path[-1], max_path[0])

    def fortify(self) -> Optional[FortAction]:
        if not self.can_fort:
            return None
        gdata = self.game.game_data
        max_path = self.attack_path
        if len(max_path) == 1:
            return get_fort_from_nodes(gdata, [max_path[0]])
        return get_fort_from_nodes(gdata, [max_path[0], max_path[-1]])

    # TODO define some theoretical basis for this
    def calculate_gain(self, opposition):
        gdata = self.game.game_data
        if opposition == gdata.player_id:
            return 0
        p_gains = gdata.get_players_troop_gain()
        other_opposition = [i for i in range(3) if i != opposition and i != gdata.player_id][0]

        my_gain, op_gain, oop_gain = [
            p_gains[x] for x in [gdata.player_id, opposition, other_opposition]
        ]
        if min(my_gain * 1.5, 10) < op_gain and gdata.phase_2_turns > 1:
            if oop_gain - my_gain > 3:
                return 0
            elif my_gain - oop_gain > 4:
                return 0.8
            else:
                return 0.5
        return 0.5

    def get_loss_node_gain_by_player(self, target: Node, player):
        gdata = self.game.game_data
        attack_power, max_path = get_surprise_danger(
            gdata, target, player, include_src_troops=True, return_max_path=True
        )
        if attack_power <= 0:
            return 0
        for node in max_path:
            node.save_version()
            node.owner = player
            node.number_of_troops = 1
            if node.id != max_path[0].id:
                node.number_of_fort_troops = 0
        win_prob = 1 - get_attack_outcomes(max_path)[0]
        score = 0
        # revert this at the end!
        original_init = gdata.remaining_init[gdata.player_id]
        gdata.remaining_init[gdata.player_id] = gdata.get_players_troop_gain()[gdata.player_id]
        if max_path[0].is_strategic:
            danger = balance_troops_between_two_strategics(gdata, max_path[0], max_path[-1], return_danger=True)[1]
            if danger <= 0:
                score = win_prob * target.score_of_strategic
            else:
                target.number_of_troops = round(
                    attack_power if gdata.players_done_fort[player] else 2 * attack_power - 1
                )
                if get_node_danger(gdata, target) <= 0:
                    score = win_prob * (target.score_of_strategic - max_path[0].score_of_strategic)
        if not max_path[0].is_strategic:
            target.number_of_troops = round(attack_power if gdata.players_done_fort[player] else 2 * attack_power - 1)
            if get_node_danger(gdata, target) <= 0:
                score = win_prob * target.score_of_strategic
        gain = self.calculate_gain(player)
        for node in max_path:
            node.restore_version()
        gdata.remaining_init[gdata.player_id] = original_init
        return max(score, 0) * gain

    def get_max_loss_node(self, node: Node):
        gdata = self.game.game_data
        return max(
            self.get_loss_node_gain_by_player(node, i) for i in range(3) if i != gdata.player_id
        )

    def get_general_attack_score(self, path: List[Node], troops_to_put):
        gdata = self.game.game_data
        score = 0
        src, tar = path[0], path[-1]
        src_danger = get_node_danger(gdata, src)

        src.number_of_troops += troops_to_put
        outcomes = get_attack_outcomes(path)

        # Don't even consider low probability attacks
        win_prob = 1 - outcomes[0]
        if win_prob < 0.7:
            src.number_of_troops -= troops_to_put
            return -np.inf

        # print(outcomes[:3])
        first_win_prob = 1 - get_attack_outcomes([path[0], path[1]])[0] if len(path) > 1 else 0
        score += 1. * first_win_prob  # no +3 strategic
        src.number_of_troops -= troops_to_put

        # TODO we can add +3 troops on successfully attacking to score too
        loss_gain_src = 0
        if src.is_strategic:
            if src.id not in self.max_loss_cache:
                self.max_loss_cache[src.id] = self.get_max_loss_node(src)
            loss_gain_src = self.max_loss_cache[src.id]

        tar_gain = tar.score_of_strategic * self.calculate_gain(tar.owner)

        attack_force = path[0].number_of_troops + troops_to_put
        for node in path:
            node.save_version()
            node.owner = gdata.player_id
            node.number_of_troops = 1
            if node.id != path[0].id:
                node.number_of_fort_troops = 0
        if src.is_strategic and src_danger <= 0:  # case of loss outcome = 0
            score -= outcomes[0] * (src.score_of_strategic + loss_gain_src)

        if len(path) == 1:
            if not src.is_strategic or src_danger <= 0:
                score += 0
            else:
                src.number_of_troops = attack_force
                if self.can_fort and not gdata.done_fort:
                    src.number_of_troops *= 2
                    src.number_of_troops -= 1
                danger = get_node_danger(gdata, src)
                if danger <= 0:
                    score += 1. * (src.score_of_strategic + loss_gain_src)
            for node in path:
                node.restore_version()
            # print(f'src: {src.score_of_strategic}, tar: {tar.score_of_strategic} -> {score}')
            return score

        # TODO can move and can fort should be accounted for here
        MAX_NUM = min(MAX_TROOP_CALC, attack_force + 1)
        max_check = MAX_NUM

        def two_nodes_danger_func(remaining_troops: int):
            if remaining_troops == 0:
                return False
            tar.number_of_troops = remaining_troops
            return check_balance_two_strategic_possible(gdata, tar, src, can_fort=self.can_fort)

        if src.is_strategic and tar.is_strategic:  # holding both
            max_check = binary_search(0, max_check, two_nodes_danger_func, left_value=False)
            for i in range(max_check, MAX_NUM):
                prob = outcomes[i]
                score += prob * (tar.score_of_strategic + tar_gain)  # holding new strategic
                score += prob * (
                        src.score_of_strategic + loss_gain_src
                ) if src_danger > 0 else 0  # saving old strategic

        # either save src or target
        def can_save_node(remaining_troops: int, save_node: Node, danger_on_one_troop):
            if remaining_troops == 0:
                return False
            exp = get_expected_casualty()
            save_node.save_version()
            save_node.number_of_troops = remaining_troops
            if self.can_fort and not gdata.done_fort:
                save_node.number_of_troops *= 2
                save_node.number_of_troops -= 1
            d = danger_on_one_troop + exp[save_node.number_of_fort_troops + 1]
            d -= exp[save_node.number_of_fort_troops + save_node.number_of_troops]
            save_node.restore_version()
            return d <= 0

        src_one_troop_danger = get_node_danger(gdata, src)
        save_src_troops = binary_search(
            0, max_check, can_save_node, False, save_node=src, danger_on_one_troop=src_one_troop_danger
        ) if src.is_strategic else MAX_NUM

        tar_one_troop_danger = get_node_danger(gdata, tar)
        save_tar_troops = binary_search(
            0, max_check, can_save_node, False, save_node=tar, danger_on_one_troop=tar_one_troop_danger
        ) if tar.is_strategic else MAX_NUM

        if src.score_of_strategic < tar.score_of_strategic:
            save_tar_range = (save_tar_troops, max_check)
            save_src_range = (min(save_src_troops, save_tar_troops), save_tar_troops)
        else:
            save_src_range = (save_src_troops, max_check)
            save_tar_range = (min(save_src_troops, save_tar_troops), save_src_troops)

        for i in range(save_tar_range[0], save_tar_range[1]):  # save target
            prob = outcomes[i]
            score += prob * (tar.score_of_strategic + tar_gain)
            if src.is_strategic and src_danger <= 0:
                score -= prob * (src.score_of_strategic + loss_gain_src)

        for i in range(save_src_range[0], save_src_range[1]):  # save src
            prob = outcomes[i]
            score += prob * tar_gain
            if src.is_strategic and src_danger > 0:
                score += prob * (src.score_of_strategic + loss_gain_src)

        for i in range(1, min(save_src_troops, save_tar_troops, max_check)):  # can't save anything
            prob = outcomes[i]
            score += prob * tar_gain
            if src.is_strategic and src_danger <= 0:
                score -= prob * (src.score_of_strategic + loss_gain_src)

        for node in path:
            node.restore_version()

        # print(f'src: {src.score_of_strategic} [{src.id}], tar: {tar.score_of_strategic} [{tar.id}] -> {score} /'
        #       f' {src.number_of_troops + troops_to_put} - {tar.number_of_troops + tar.number_of_fort_troops}')
        return score

    def check_attack_pairs(self, max_troops_to_put=None):
        gdata = self.game.game_data
        graph = gdata.get_passable_board_graph(gdata.player_id)
        strategic_nodes = [node for node in gdata.nodes if node.owner != gdata.player_id and node.is_strategic]
        troops_to_put = gdata.remaining_init[gdata.player_id]
        if max_troops_to_put is not None:
            troops_to_put = min(troops_to_put, max_troops_to_put)

        plan = (None, (-np.Inf, -np.Inf, -np.Inf))

        for target in strategic_nodes:
            paths = nx.shortest_path(graph, target=target.id, weight='weight')
            paths_length = nx.shortest_path_length(graph, target=target.id, weight='weight')
            for src in [n for n in gdata.nodes if n.owner in [gdata.player_id, None] and n.id in paths]:
                attack_power = src.number_of_troops + troops_to_put - paths_length[src.id]
                path = [gdata.nodes[x] for x in paths[src.id]]
                score = self.get_general_attack_score(path, troops_to_put)
                score = (score, -paths_length[src.id], attack_power)
                if plan[1] < score:
                    plan = (path, score)

        for src in [n for n in gdata.nodes if n.owner == gdata.player_id and n.is_strategic]:
            paths = nx.shortest_path(graph, source=src.id, weight='weight')
            paths_length = nx.shortest_path_length(graph, source=src.id, weight='weight')
            for target in [
                n for n in gdata.nodes
                # already check other strategics
                if n.owner not in [gdata.player_id, None] and n.id in paths and not n.is_strategic
            ]:
                if paths_length[target.id] > 3:
                    continue
                attack_power = src.number_of_troops + troops_to_put - paths_length[target.id]
                path = [gdata.nodes[x] for x in paths[target.id]]
                score = self.get_general_attack_score(path, troops_to_put)
                score = (score, -paths_length[target.id], attack_power)
                if plan[1] < score:
                    plan = (path, score)

            attack_power = src.number_of_troops + troops_to_put
            score = self.get_general_attack_score([src], troops_to_put)
            score = (score, 0, attack_power)
            if plan[1] < score:
                plan = ([src], score)

        return plan

    def check_only_capture_attack(self, bypass_by_owner=None):
        gdata = self.game.game_data
        strategic_nodes = [node for node in gdata.nodes if node.owner != gdata.player_id and node.is_strategic]

        max_attack_power, max_path = 0, None
        for st_node in strategic_nodes:
            attack_power, path = get_surprise_danger(
                gdata, st_node, gdata.player_id,
                return_max_path=True, include_src_troops=True
            )
            if len(path) == 0:
                continue
            if bypass_by_owner is not None and path[-1].owner != bypass_by_owner:
                continue
            if max_attack_power < attack_power:
                max_attack_power = attack_power
                max_path = path
        return max_path, max_attack_power

    def compute_plan(self, attempt=0, can_move=True, can_fort=True):
        gdata = self.game.game_data
        self.can_move = can_move
        self.can_fort = can_fort
        strategic_nodes = [node for node in gdata.nodes if node.owner != gdata.player_id and node.is_strategic]
        my_strategic = [node for node in gdata.nodes if node.owner == gdata.player_id and node.is_strategic]

        bypass_danger = len(my_strategic) == 3 and gdata.turn_number >= 126
        max_strategics, max_owner = -1, None
        for pi in range(gdata.player_cnt):
            if pi == gdata.player_id:
                continue
            num_stra = len([n for n in strategic_nodes if n.owner == pi])
            if max_strategics < num_stra:
                max_strategics = num_stra
                max_owner = pi
        # last chance before losing
        if gdata.turn_number >= 124 and max_strategics >= 4:
            bypass_danger = True

        chosen_plan = (None, (0, -np.Inf, -np.Inf))
        if bypass_danger:
            chosen_plan = self.check_only_capture_attack(
                bypass_by_owner=max_owner if max_strategics >= 4 else None
            )
        else:
            chosen_plan = self.check_attack_pairs(gdata.remaining_init[gdata.player_id])
            if chosen_plan[1][0] < 1:
                chosen_plan = (None, (0, -np.Inf, -np.Inf))
        self.troops_to_put = gdata.remaining_init[gdata.player_id]
        self.attack_path = chosen_plan[0]
        return self.attack_path is not None

clients/strategy/one_surprise_attack.py
AICUP2023-Hybrid/Agent
from clients.strategy.startegy import *
from clients.utils.attack_chance import get_expected_casualty, get_expected_casualty_by_troops, get_win_rate
from math import floor


class Plus3Strategy(Strategy):
    def __init__(self, game: GameClient | Game):
        super().__init__(game)
        self.troops_to_put = 0
        self.src = None
        self.tar = None

    def put_troops(self) -> List[PutTroopsAction]:
        put_troops_action = PutTroopsAction(node=self.src, number_of_troops=self.troops_to_put)

        return [put_troops_action]

    def attacks(self) -> List[AttackAction]:
        attack = AttackAction(src=self.src, dest=self.tar, fraction=0,
                              move_fraction=0.001 if self.src.is_strategic else 0.999)
        return [attack]

    def move_troop(self) -> Optional[MoveTroopAction]:
        return None

    def fortify(self) -> Optional[FortAction]:
        return None

    def compute_plan(self, attempt=0):
        gdata = self.game.game_data
        # +3 force attack
        max_score, src, tar, src_troops_to_put = 0, None, None, 0

        max_range = 1
        if attempt == 0:
            max_range = 4
        elif attempt == 1:
            max_range = 1

        for node in gdata.nodes:
            if node.owner not in [gdata.player_id, None]:
                continue
            for troops_to_put in range(max_range):
                for nei in node.adj_main_map:
                    if nei.owner not in [None, gdata.player_id]:
                        attacking_troops = node.number_of_troops + troops_to_put
                        defending_troops = nei.number_of_troops + nei.number_of_fort_troops
                        if defending_troops > 5:  # speed up
                            continue
                        attacking_troops = min(attacking_troops, 8)  # speed up
                        casualty = get_expected_casualty_by_troops(attacking_troops, defending_troops) + 1
                        new_troop_remain = troops_to_put - max(0, casualty - node.number_of_troops)
                        loss = casualty + 0.15 * new_troop_remain
                        if 3 - loss < max_score:  # speed up
                            continue
                        win_prob = get_win_rate(attacking_troops, defending_troops)
                        score = win_prob * 3 - loss
                        if max_score < score:
                            src_troops_to_put = troops_to_put
                            max_score = score
                            src = node
                            tar = nei
        self.troops_to_put = src_troops_to_put
        self.src = src
        self.tar = tar
        return tar is not None

clients/strategy/plus3_strategy.py
AICUP2023-Hybrid/Agent
import dataclasses
from abc import abstractmethod
from typing import List, Optional

from clients.utils.maximize_score import maximize_score
from components.node import Node

from clients.game_client import GameClient
from online_src.game import Game

f = open(f'log0.txt', 'w')


@dataclasses.dataclass
class PutTroopsAction:
    node: Node
    number_of_troops: int


@dataclasses.dataclass
class AttackAction:
    src: Node
    dest: Node
    fraction: float
    move_fraction: float


@dataclasses.dataclass
class MoveTroopAction:
    src: Node
    dest: Node
    count: int


@dataclasses.dataclass
class FortAction:
    node: Node
    count: int


class Strategy:
    def __init__(self, game: GameClient | Game):
        self.game = game

    @abstractmethod
    def put_troops(self) -> List[PutTroopsAction]:
        pass

    @abstractmethod
    def attacks(self) -> List[AttackAction]:
        pass

    @abstractmethod
    def move_troop(self) -> Optional[MoveTroopAction]:
        pass

    @abstractmethod
    def fortify(self) -> Optional[FortAction]:
        pass

    @abstractmethod
    def compute_plan(self, attempt=0):
        pass

    def run_put_troops_strategy(self, go_next_state=True):
        gdata = self.game.game_data
        remaining_troops = gdata.game.get_number_of_troops_to_put()['number_of_troops']
        for put_troop in self.put_troops():
            if put_troop.number_of_troops > 0:
                self.game.put_troop(put_troop.node.id,
                                    min(remaining_troops, put_troop.number_of_troops))

        gdata.update_game_state()
        if go_next_state:
            self.game.next_state()

    def run_attack_strategy(self, go_next_state=True):
        gdata = self.game.game_data
        attack_succeeded = True
        for attack in self.attacks():
            if attack.src.owner != self.game.game_data.player_id or attack.src.number_of_troops < 2:
                attack_succeeded = False
                continue
            self.game.attack(attack.src.id, attack.dest.id, attack.fraction, attack.move_fraction)
            gdata.update_game_state()

            my_strategics = [node for node in gdata.nodes if node.is_strategic and node.owner == gdata.player_id]
            if len(my_strategics) >= 4 and gdata.turn_number >= 126:
                maximize_score(self.game, can_put_troops=False)
                break

        if go_next_state:
            self.game.next_state()
        return attack_succeeded

    def run_move_troops_strategy(self, go_next_state=True):
        gdata = self.game.game_data
        move_troops = self.move_troop()
        if move_troops:
            self.game.move_troop(move_troops.src.id, move_troops.dest.id, move_troops.count)

        gdata.update_game_state()

        if go_next_state:
            self.game.next_state()

    def run_fortify_strategy(self, go_next_state=True):
        gdata = self.game.game_data
        fortify = self.fortify()
        if fortify:
            self.game.fort(fortify.node.id, fortify.count)
            gdata.done_fort = True

        gdata.update_game_state()

        if go_next_state:
            self.game.next_state()

    def run_strategy(self):
        self.run_put_troops_strategy(go_next_state=True)

        self.run_attack_strategy(go_next_state=True)

        self.run_move_troops_strategy(go_next_state=True)

        self.run_fortify_strategy(go_next_state=True)

    def run_strategy_until_feasible(self, n_tries=1):
        gdata = self.game.game_data
        # initial attempt (put troops section)
        plan_exists = self.compute_plan(attempt=0)
        if not plan_exists:
            return -1
        self.run_put_troops_strategy(go_next_state=True)
        cnt = 0
        while plan_exists:
            self.run_attack_strategy(go_next_state=False)
            self.game.next_state()
            self.run_move_troops_strategy(go_next_state=True)
            self.run_fortify_strategy(go_next_state=True)

            cnt += 1
            if cnt == n_tries:
                return cnt
            # find a plan again
            plan_exists = self.compute_plan(attempt=1)
        return cnt

    def run_strategy_until_success(self):
        gdata = self.game.game_data
        # initial attempt (put troops section)
        plan_exists = self.compute_plan(attempt=0)
        if not plan_exists:
            return False
        self.run_put_troops_strategy(go_next_state=True)
        cnt = 0
        while plan_exists:
            attack_succeeded = self.run_attack_strategy(go_next_state=False)
            if attack_succeeded:
                self.game.next_state()
                self.run_move_troops_strategy(go_next_state=True)
                self.run_fortify_strategy(go_next_state=True)
                return True
            # find a plan again if the attack didn't succeed
            plan_exists = self.compute_plan(attempt=1)
            cnt += 1
        return False

clients/strategy/startegy.py
AICUP2023-Hybrid/Agent
import networkx as nx
from clients.strategy.startegy import *
from clients.strategy.utils.path_attack_sequence import get_one_path_attack_sequence
from clients.utils.get_possible_danger import get_two_way_attack


class TwoSurpriseAttack(Strategy):
    def __init__(self, game: GameClient | Game, condition):
        super().__init__(game)
        self.candidate = None
        self.l1 = 0
        self.l2 = 0
        self.condition = condition

    def put_troops(self) -> List[PutTroopsAction]:
        gdata = self.game.game_data
        candidate = self.candidate
        l1, l2 = -candidate[6], -candidate[7]
        remaining_troops = gdata.remaining_init[gdata.player_id]

        put_troops_action_list = []

        if candidate[1] == 0:
            s1: Node = candidate[2]
            s2: Node = candidate[3]
            p1, p2 = 0, 0
            tmp = remaining_troops
            while tmp > 0:
                if l1 < l2:
                    p2 += 1
                    l2 -= 1
                else:
                    p1 += 1
                    l1 -= 1
                tmp -= 1
            if p1 > 0:
                put_troops_action = PutTroopsAction(node=s1, number_of_troops=p1)
                put_troops_action_list.append(put_troops_action)
            if p2 > 0:
                put_troops_action = PutTroopsAction(node=s2, number_of_troops=p2)
                put_troops_action_list.append(put_troops_action)
        else:
            s: Node = candidate[2]
            if remaining_troops > 0:
                put_troops_action = PutTroopsAction(node=s, number_of_troops=remaining_troops)
                put_troops_action_list.append(put_troops_action)
        self.l1 = l1
        self.l2 = l2
        return put_troops_action_list

    def attacks(self) -> List[AttackAction]:
        gdata = self.game.game_data
        candidate = self.candidate
        attacks = []
        graph = gdata.get_passable_board_graph(gdata.player_id)
        if candidate[1] == 0:
            t1, t2 = candidate[4], candidate[5]
            s1: Node = candidate[2]
            s2: Node = candidate[3]

            path1 = [gdata.nodes[x] for x in nx.shortest_path(graph, s1.id, t1.id, weight='weight')]
            path2 = [gdata.nodes[x] for x in nx.shortest_path(graph, s2.id, t2.id, weight='weight')]
            attacks.extend(get_one_path_attack_sequence(path1))
            attacks.extend(get_one_path_attack_sequence(path2))
        else:
            t1, t2 = candidate[4], candidate[5]
            s: Node = candidate[2]
            mid: Node = candidate[3]

            path = [gdata.nodes[x] for x in nx.shortest_path(graph, s.id, mid.id, weight='weight')]
            attacks.extend(get_one_path_attack_sequence(path))
            path1 = [gdata.nodes[x] for x in nx.shortest_path(graph, mid.id, t1.id, weight='weight')]
            path2 = [gdata.nodes[x] for x in nx.shortest_path(graph, mid.id, t2.id, weight='weight')]
            if len(path1) > 1:
                # print('boooooooooom')
                next_node = path1[1]
                l1p = max(0, self.l1 - graph.edges[mid.id, next_node.id]['weight'])
                l2p = max(0, self.l2)
                attack = AttackAction(mid, next_node, fraction=0,
                                      move_fraction=max(0.001, min(0.999,
                                                                   l1p / (l1p + l2p) if l1p + l2p > 0 else 1
                                                                   )))
                attacks.append(attack)
                attacks.extend(get_one_path_attack_sequence(path1[1:]))
            attacks.extend(get_one_path_attack_sequence(path2))
        return attacks

    def move_troop(self) -> Optional[MoveTroopAction]:
        return None

    def fortify(self) -> Optional[FortAction]:
        return None

    def compute_plan(self, attempt=0):
        gdata = self.game.game_data

        strategic_nodes = [node for node in gdata.nodes if node.owner != gdata.player_id and node.is_strategic]

        consider_condition = self.condition
        if not consider_condition:
            return False

        for node_st1 in strategic_nodes:
            for node_st2 in strategic_nodes:
                if node_st1.id == node_st2.id:
                    continue
                attack_plan = get_two_way_attack(gdata, node_st1, node_st2)
                if attack_plan is None:
                    continue
                if self.candidate is None or self.candidate[0] < attack_plan[0]:
                    self.candidate = attack_plan

        consider_condition = self.candidate is not None
        if not consider_condition:
            return False

        return True

clients/strategy/two_surprise_attack.py
AICUP2023-Hybrid/Agent
import networkx as nx
import numpy as np

from clients.strategy.startegy import MoveTroopAction
from clients.utils.get_possible_danger import get_node_danger
from clients.utils.update_details import GameData
from components.node import Node


def get_node_attack_advantage(gdata: GameData, node: Node):
    graph = gdata.get_passable_board_graph(node.number_of_troops)
    shortest_paths = nx.shortest_path_length(graph, source=node.id, weight="weight")
    targets = [n for n in gdata.nodes if n.is_strategic and n.owner != node.owner and n.id in shortest_paths]

    score = 0
    for target in targets:
        score = max(score, node.number_of_troops - shortest_paths[target.id])
    return score


def balance_troops_between_two_strategics(gdata: GameData, src: Node, dst: Node, can_fort=True, return_danger=False):
    troop_cnt = src.number_of_troops
    player = src.owner

    src.save_version()
    dst.save_version()
    min_danger, move_troops = np.Inf, 0
    for fort_node in ([src, dst] if not gdata.players_done_fort[player] and can_fort else [None]):
        for i in range(0, troop_cnt):
            src.number_of_troops = troop_cnt - i
            dst.number_of_troops = i + 1
            if fort_node:
                fort_node.number_of_troops *= 2
                fort_node.number_of_troops -= 1

            src_strategic_danger = get_node_danger(gdata, src)
            dst_strategic_danger = get_node_danger(gdata, dst)
            danger = max(src_strategic_danger, dst_strategic_danger)

            if danger < min_danger:
                move_troops = i
                min_danger = danger
    src.restore_version()
    dst.restore_version()

    move = None
    if min_danger > 0 and dst.score_of_strategic > src.score_of_strategic:
        move = MoveTroopAction(src=src, dest=dst, count=troop_cnt - 1)
    if min_danger <= 0 < move_troops:
        move = MoveTroopAction(src=src, dest=dst, count=move_troops)
    if return_danger:
        return move, min_danger
    return move


def check_balance_two_strategic_possible(gdata: GameData, src: Node, dst: Node, can_fort=True):
    troop_cnt = src.number_of_troops
    player = src.owner

    fort_possible = can_fort and not gdata.players_done_fort[player]
    src.number_of_troops = troop_cnt * 2 - 1 if fort_possible else troop_cnt
    dst.number_of_troops = 1
    danger_src = get_node_danger(gdata, src)
    if danger_src > 0:
        return False
    start_at = int(-danger_src / (0.7 * 2 if fort_possible else 0.7))

    src.save_version()
    dst.save_version()
    for fort_node in ([src, dst] if not gdata.players_done_fort[player] and can_fort else [None]):
        prev_src_danger, prev_dst_danger = None, None
        for i in range(start_at, troop_cnt):
            src.number_of_troops = troop_cnt - i
            dst.number_of_troops = i + 1
            if fort_node:
                fort_node.number_of_troops *= 2
                fort_node.number_of_troops -= 1

            src_strategic_danger = get_node_danger(gdata, src)
            dst_strategic_danger = get_node_danger(gdata, dst)
            danger = max(src_strategic_danger, dst_strategic_danger)

            if danger <= 0:
                src.restore_version()
                dst.restore_version()
                return True

            if prev_src_danger is not None:
                if src_strategic_danger > prev_src_danger:
                    break

            prev_src_danger, prev_dst_danger = src_strategic_danger, dst_strategic_danger
    src.restore_version()
    dst.restore_version()

    return False

clients/strategy/utils/balance_two_strategic.py
AICUP2023-Hybrid/Agent
from typing import List, Optional

from clients.strategy.startegy import FortAction
from clients.utils.attack_chance import get_expected_casualty
from clients.utils.get_possible_danger import get_node_danger
from clients.utils.update_details import GameData
from components.node import Node


def get_fort_from_nodes(gdata: GameData, nodes: List[Node]):
    nodes = [
        node for node in nodes if node.is_strategic and node.owner == gdata.player_id and node.number_of_troops > 1
    ]
    if gdata.done_fort or len(nodes) == 0:
        return None

    expected_causalty = get_expected_casualty()
    possibilities = []
    for target in nodes:
        danger = get_node_danger(gdata, target)
        if danger <= 0:
            continue
        total_count = target.number_of_troops
        fortify_count = None
        for to_fortify in range(total_count):
            delta = expected_causalty[target.number_of_troops + to_fortify] - expected_causalty[target.number_of_troops]
            if danger - delta <= 0:
                fortify_count = to_fortify
            if danger - delta <= -4:  # TODO tune this hyperparameter
                break
        if fortify_count:
            possibilities.append(FortAction(node=target, count=fortify_count))
    best_option: Optional[FortAction] = None
    for pos in possibilities:
        better = best_option is None
        better = better or pos.node.score_of_strategic > best_option.node.score_of_strategic
        if better:
            best_option = pos
    return best_option

clients/strategy/utils/choose_fort_nodes.py
AICUP2023-Hybrid/Agent
from clients.strategy.startegy import AttackAction


def get_one_path_attack_sequence(path):
    attack_actions = []
    for i in range(len(path) - 1):
        attack = AttackAction(src=path[i], dest=path[i + 1], fraction=0, move_fraction=0.999)
        attack_actions.append(attack)
    return attack_actions

clients/strategy/utils/path_attack_sequence.py
AICUP2023-Hybrid/Agent
def balance_two_nodes_binary_search(left, right, danger_func, **kwargs):
    d = danger_func(right - 1, **kwargs)
    if d > 0:
        return right
    while left < right - 1:
        x = (right + left) // 2
        d = danger_func(x, **kwargs)
        x += (d / 0.7) // 2
        x = int(max(left, min(right - 1, x)))
        if d <= 0:
            right = x
        else:
            left = x
    return left + 1  # first one that is not left value


def binary_search(left, right, truth_func, left_value, **kwargs):
    while left < right - 1:
        x = (right + left) // 2
        if truth_func(x, **kwargs) == left_value:
            left = x
        else:
            right = x
    return left + 1  # first one that is not left value

clients/utils/algorithms.py
AICUP2023-Hybrid/Agent
import os.path
from typing import List

import numpy as np
from collections import Counter

from components.node import Node

attacker_dice_probs = dict()
for attacker_dice in range(4):
    defender_dice_probs = dict()
    for defender_dice in range(3):
        probs = Counter()
        cnt = 0

        for pos in range(6 ** (attacker_dice + defender_dice)):
            cur = pos
            attacker_ls = []
            defender_ls = []
            for i in range(attacker_dice):
                attacker_ls.append(cur % 6 + 1)
                cur /= 6
            for i in range(defender_dice):
                defender_ls.append(cur % 6 + 1)
                cur /= 6
            attacker_ls = sorted(attacker_ls, reverse=True)
            defender_ls = sorted(defender_ls, reverse=True)

            attack_loss = 0
            defend_loss = 0
            for i in range(min(attacker_dice, defender_dice)):
                if attacker_ls[i] > defender_ls[i]:
                    defend_loss += 1
                else:
                    attack_loss += 1
            probs[(attack_loss, defend_loss)] += 1
            cnt += 1

        probs_norm = dict()
        for k in probs.keys():
            probs_norm[k] = probs[k] / cnt
        defender_dice_probs[defender_dice] = probs_norm

    attacker_dice_probs[attacker_dice] = defender_dice_probs


def calc_dp(attacker_troops: int, defender_troops: int, fraction=None):
    dp = np.zeros((attacker_troops + 1, defender_troops + 1))
    dp[attacker_troops, defender_troops] = 1
    for att_cnt in range(attacker_troops, -1, -1):
        for def_cnt in range(defender_troops, -1, -1):
            att_dice = min(3, att_cnt - 1)
            def_dice = min(2, def_cnt)
            if att_dice <= 0 or def_dice <= 0 or (fraction is not None and att_cnt < def_cnt * fraction):
                continue

            for pos, prob in attacker_dice_probs[att_dice][def_dice].items():
                dp[max(0, att_cnt - pos[0]), max(0, def_cnt - pos[1])] += prob * dp[att_cnt][def_cnt]
    return dp


def get_chances(attacker_troops: int, defender_troops: int, fraction):
    dp = calc_dp(attacker_troops, defender_troops, fraction)
    outcomes = []
    for att_cnt in range(attacker_troops, -1, -1):
        for def_cnt in range(defender_troops, -1, -1):
            att_dice = min(3, att_cnt - 1)
            def_dice = min(2, def_cnt)
            if att_dice <= 0 or def_dice <= 0 or att_cnt < def_cnt * fraction:
                if dp[att_cnt][def_cnt] > 0:
                    outcomes.append(((att_cnt, def_cnt), dp[att_cnt][def_cnt]))
    return outcomes


expected_casualty = []


def get_expected_casualty():
    global expected_casualty
    if len(expected_casualty) == 0:
        expected_casualty = []
        for d in range(40):
            expected_remain = 0
            for pr, prob in get_chances(d + 15, d, 0):
                expected_remain += pr[0] * prob
            expected_casualty.append(d + 15 - expected_remain)
        for i in range(40, 400):
            expected_casualty.append(0.688 * i)  # good approx
    return expected_casualty


def get_expected_casualty_by_troops(attack_troops, defend_troops):
    all_pos = get_chances(attack_troops, defend_troops, 0)
    exp = 0
    for (attack, defence), possibility in all_pos:
        exp += possibility * (attack_troops - attack)
    return exp


attack_outcome_table = None
MAX_TROOP_CALC = 100


# table is indexed like this [defender_troops, attack_troops, attack_remaining] = prob of attack remaining outcome
def get_attacker_outcome_table():
    global attack_outcome_table
    if attack_outcome_table is not None:
        return attack_outcome_table
    path = './clients/utils/attack_outcomes.npy'
    if os.path.exists(path):
        attack_outcome_table = np.load(path)
    else:
        attack_outcome_table = np.zeros((MAX_TROOP_CALC, MAX_TROOP_CALC, MAX_TROOP_CALC))
        for defender in range(MAX_TROOP_CALC):
            for attacker in range(MAX_TROOP_CALC):
                outcomes = get_chances(attacker, defender, 0)
                for (att_cnt, def_cnt), prob in outcomes:
                    attack_outcome_table[defender][attacker][att_cnt] += prob
        np.save(path, attack_outcome_table)
    return attack_outcome_table


def get_attack_outcomes(path: List[Node]):
    attack_outcomes = get_attacker_outcome_table()
    cur_outcome = np.zeros((MAX_TROOP_CALC,))
    cur_outcome[min(MAX_TROOP_CALC - 1, path[0].number_of_troops)] = 1.
    for node in path[1:]:
        defenders = min(MAX_TROOP_CALC - 1, node.number_of_troops + node.number_of_fort_troops)
        cur_outcome = np.matmul(cur_outcome, attack_outcomes[defenders])
        cur_outcome = np.roll(cur_outcome, -1)
        cur_outcome[0] += cur_outcome[-1]
        cur_outcome[-1] = 0
    return cur_outcome


def get_win_rate(attack_troops, defend_troops):
    attack_troops = min(attack_troops, MAX_TROOP_CALC - 1)
    defend_troops = min(defend_troops, MAX_TROOP_CALC - 1)
    loss = get_attacker_outcome_table()[defend_troops][attack_troops][0]
    loss += get_attacker_outcome_table()[defend_troops][attack_troops][1]
    return 1 - loss

clients/utils/attack_chance.py
AICUP2023-Hybrid/Agent
from collections import defaultdict

import networkx as nx
import numpy as np
from networkx import NetworkXNoPath

from clients.utils.attack_chance import get_expected_casualty
from clients.utils.update_details import GameData
from components.node import Node


def get_min_loss_path(gdata: GameData, target: Node, player,
                      max_troops_to_put=None, attack_power_threshold=0):
    graph = gdata.get_passable_board_graph(player)
    gdata.update_remaining_troops_by_map()
    if max_troops_to_put is None:
        remaining_troops = gdata.remaining_init[player]
    else:
        remaining_troops = min(max_troops_to_put, gdata.remaining_init[player])

    distances = nx.shortest_path_length(graph, target=target.id, weight='weight')
    min_loss = np.Inf
    min_path, min_src = [], None
    for src in gdata.nodes:
        if src.owner not in [player, None] or src.id not in distances:
            continue
        attack_power = gdata.nodes[src.id].number_of_troops + remaining_troops - distances[src.id]
        if attack_power_threshold <= attack_power and distances[src.id] < min_loss:
            min_loss = distances[src.id]
            min_src = src
    if min_src is not None:
        min_path = [gdata.nodes[x] for x in nx.shortest_path(graph, min_src.id, target.id, weight='weight')]
    return min_loss, min_path


def get_surprise_danger(gdata: GameData, target: Node, player, return_max_path=False, include_src_troops=False,
                        max_troops_to_put=None):
    graph = gdata.get_passable_board_graph(player)
    gdata.update_remaining_troops_by_map()
    if max_troops_to_put is None:
        remaining_troops = gdata.remaining_init[player]
    else:
        remaining_troops = min(max_troops_to_put, gdata.remaining_init[player])

    distances = nx.shortest_path_length(graph, target=target.id, weight='weight')
    max_attack_power = -np.Inf
    max_path, max_src = [], None
    for src in gdata.nodes:
        if src.owner not in [player, None] or src.id not in distances:
            continue
        attack_power = remaining_troops - distances[src.id]
        if include_src_troops:
            attack_power += gdata.nodes[src.id].number_of_troops
        if max_attack_power < attack_power:
            max_attack_power = attack_power
            max_src = src
    if return_max_path:
        if max_src is not None:
            max_path = [gdata.nodes[x] for x in nx.shortest_path(graph, max_src.id, target.id, weight='weight')]
        return max_attack_power, max_path
    return max_attack_power


def get_targets_by_attacker(gdata: GameData, attacker_id):
    res = []
    for node in [node for node in gdata.nodes if node.is_strategic]:
        if node.owner == attacker_id:
            continue
        res.append((
            get_surprise_danger(gdata, node, attacker_id, include_src_troops=True),
            node
        ))
    return sorted(res, key=lambda x: x[0], reverse=True)


def get_node_danger(gdata: GameData, node: Node, return_path=False, max_troops_to_expect=None):
    # return get_relative_strategic_node_danger(gdata, node.owner)[node]
    max_attack_power, path = -np.Inf, None
    for player in range(gdata.player_cnt):
        if player == node.owner:
            continue
        res = get_surprise_danger(gdata, node, player,
                                  include_src_troops=True,
                                  max_troops_to_put=max_troops_to_expect,
                                  return_max_path=return_path)
        if return_path:
            attack_power = res[0]
        else:
            attack_power = res
        if max_attack_power < attack_power:
            max_attack_power = attack_power
            if return_path:
                path = res[1]

    if return_path:
        return max_attack_power, path
    return max_attack_power  # returns expected amount of troops left after being attacked


def get_relative_strategic_node_danger(gdata: GameData, defender_id):
    node_dangers = defaultdict(lambda: 0)
    for attacker_id in range(gdata.player_cnt):
        if attacker_id == defender_id:
            continue
        non_defender_danger = 0
        res = get_targets_by_attacker(gdata, attacker_id)
        for danger, node in res:
            if node.owner != defender_id:
                non_defender_danger = max(danger, non_defender_danger)
        for danger, node in res:
            if node.owner == defender_id:
                node_dangers[node] = max(node_dangers[node], danger - non_defender_danger)
    return node_dangers


def get_two_way_attack(gdata: GameData, node_st1: Node, node_st2: Node):
    graph: nx.DiGraph = gdata.get_passable_board_graph(gdata.player_id)
    remaining_troops = gdata.remaining_init[gdata.player_id]
    candidate = None

    # two separated the shortest paths
    n1_paths, n2_paths = [nx.shortest_path_length(graph, target=n.id, weight='weight') for n in [node_st1, node_st2]]
    mx1, mx2 = -np.Inf, -np.Inf
    s1, s2 = None, None
    for node in gdata.nodes:
        if node.id not in n1_paths or node.id not in n2_paths:
            continue
        if node.owner in [None, gdata.player_id]:
            p1 = node.number_of_troops - n1_paths[node.id]
            p2 = node.number_of_troops - n2_paths[node.id]
            if mx1 < p1:
                s1 = node
                mx1 = p1
            if mx2 < p2:
                s2 = node
                mx2 = p2
    if None not in [s1, s2] and s1.id != s2.id:
        candidate = (min(0, mx1) + min(0, mx2), 0, s1, s2, node_st1, node_st2, mx1, mx2)  # (score, type, src1, src2)

    for mid_node in gdata.nodes:
        s_dist = nx.shortest_path_length(graph, target=mid_node.id, weight='weight')
        e_dist = nx.shortest_path_length(graph, source=mid_node.id, weight='weight')
        if node_st1.id not in e_dist or node_st2.id not in e_dist:
            continue
        b, c = e_dist[node_st1.id], e_dist[node_st2.id]
        for src, dist in s_dist.items():
            if gdata.nodes[src].owner in [None, gdata.player_id]:
                a = gdata.nodes[src].number_of_troops - dist
                if candidate is None or candidate[0] < a - b - c:
                    candidate = (a - b - c, 1, gdata.nodes[src], mid_node, node_st1, node_st2, -b, -c)

    if candidate is not None:
        score = remaining_troops + candidate[0]
        if score < 2:
            return None
    return candidate

clients/utils/get_possible_danger.py
AICUP2023-Hybrid/Agent
from collections import defaultdict
from typing import List, Set

import networkx as nx
from math import ceil, floor

from clients.game_client import GameClient
from clients.utils.attack_chance import get_expected_casualty
from components.node import Node


def maximize_score(game: GameClient, can_put_troops=True):
    gdata = game.game_data
    remaining_troops = gdata.remaining_init[gdata.player_id]
    if not can_put_troops:
        remaining_troops = 0
    puts = defaultdict(lambda: 0)

    for node in gdata.nodes:
        node.save_version()

    cur_nodes = [node.id for node in gdata.nodes if node.owner == gdata.player_id]
    for node in gdata.nodes:
        if node.owner is None and remaining_troops > 0:
            cur_nodes.append(node.id)
            node.number_of_troops = 1
            remaining_troops -= 1
            puts[node.id] += 1

    graph = gdata.get_passable_board_graph(gdata.player_id)
    attack_graph = nx.DiGraph()
    src_node = [node if node.id in cur_nodes else None for node in gdata.nodes]
    potential = [node.number_of_troops - 1 if node.id in cur_nodes else 0 for node in gdata.nodes]

    for node in graph.nodes:
        attack_graph.add_node(node)

    while True:
        max_attack_power, par, tar, w = -1000, None, None, None
        for u, v, a in graph.edges(data=True):
            weight = a['weight']
            if u not in cur_nodes or v in cur_nodes:
                continue
            pot = potential[src_node[u].id]
            attack_power = pot - weight
            if attack_power >= max_attack_power and weight <= pot + remaining_troops:
                max_attack_power = attack_power
                w = weight
                par = gdata.nodes[u]
                tar = gdata.nodes[v]
        if max_attack_power == -1000:
            break
        attack_graph.add_edge(par.id, tar.id, weight=w)
        src = src_node[par.id]
        src_node[tar.id] = src_node[par.id]
        left = ceil(max(0, w - potential[src.id]))
        potential[src.id] = floor(max(0, potential[src.id] - w))
        cur_nodes.append(tar.id)
        remaining_troops -= left
        puts[src.id] += left

    while remaining_troops > 0 and len(puts) > 0:
        for node_id, put_troops in puts.items():
            puts[node_id] += 1
            remaining_troops -= 1
            if remaining_troops == 0:
                break

    for node in gdata.nodes:
        node.restore_version()

    exp_cas = [0 for node in attack_graph.nodes]
    sorted_nodes = reversed(list(nx.topological_sort(attack_graph)))
    expected_casualty = get_expected_casualty()
    for node in sorted_nodes:
        exp_cas[node] += expected_casualty[gdata.nodes[node].number_of_troops]
        for nei in attack_graph.neighbors(node):
            exp_cas[node] += exp_cas[nei]

    if can_put_troops:
        for node_id, put_troops in puts.items():
            game.put_troop(node_id, put_troops)
        gdata.update_game_state()
        game.next_state()

    for node_id in nx.topological_sort(attack_graph):
        node = gdata.nodes[node_id]
        exp_sum = sum([exp_cas[nei] for nei in attack_graph.neighbors(node_id)])
        has_attack = False
        for nei in attack_graph.neighbors(node_id):
            # print(game.attack(node.id, nei, 1, exp_cas[nei] / exp_sum), file=log_file)
            try:
                game.attack(node.id, nei, 0, max(0.001, min(0.999, exp_cas[nei] / exp_sum)))
                has_attack = True
            except:
                pass
            exp_sum -= exp_cas[nei]
        if has_attack:
            gdata.update_game_state()

    while True:
        gdata.update_game_state()

        attack_round_nodes: Set[Node] = set()
        for node in gdata.nodes:
            if node.owner == gdata.player_id and node.number_of_troops > 1 and node not in attack_round_nodes:
                for nei in sorted(node.adj_main_map, key=lambda n: n.number_of_troops):
                    if nei.owner != gdata.player_id and nei not in attack_round_nodes:
                        try:
                            src_neighbours = _get_unowned_neighbours(node, gdata.player_id)
                            dst_neighbours = _get_unowned_neighbours(nei, gdata.player_id)
                            move_fraction = len(dst_neighbours - src_neighbours) / max(
                                len((dst_neighbours | src_neighbours) - set(nei)), 1)
                            game.attack(node.id, nei.id, 0, _std_mv_frac(move_fraction))
                            attack_round_nodes.add(node)
                            attack_round_nodes.add(nei)
                            break
                        except:
                            pass

        if len(attack_round_nodes) == 0:
            break


def _get_unowned_neighbours(node: Node, player_id) -> Set[Node]:
    return set(nei for nei in node.adj_main_map if nei.owner != player_id)


def _std_mv_frac(frac: float):
    return max(min(frac, 0.999), 0.001)

clients/utils/maximize_score.py
AICUP2023-Hybrid/Agent
import copy
from typing import List

import networkx as nx
import math
import numpy as np

from clients.game_client import GameClient
from clients.utils.attack_chance import get_expected_casualty
from components.node import Node
from collections import defaultdict
from online_src.game import Game as OnlineGameClient


class GameData:
    def __init__(self, game: GameClient | OnlineGameClient):
        self.nodes: List[Node] = []
        self.player_id = None
        self.player_cnt = 3
        self.game = game
        self.remaining_init = [35, 35, 35]
        self.temp_phase0_remains = [0, 0, 0]
        self.later_added = [0, 0, 0]
        self.stage = 0
        self.phase_2_turns = 1
        self.turn_number = None
        self.done_fort = False
        self.players_done_fort = [False, False, False]

    def update_game_state(self):
        if len(self.nodes) == 0:
            self.player_id = self.game.get_player_id()

            adjs = [z[1] for z in sorted(self.game.get_adj().items(), key=lambda x: int(x[0]))]
            for i in range(len(adjs)):
                node = Node(i)
                self.nodes.append(node)
            for idx, adj in enumerate(adjs):
                for adj_idx in adj:
                    self.nodes[idx].adj_main_map.append(self.nodes[adj_idx])

            strategic_data = self.game.get_strategic_nodes()
            strategic_nodes = strategic_data['strategic_nodes']
            scores = strategic_data['score']
            strategic_nodes = list(zip(strategic_nodes, scores))
            for node, score in strategic_nodes:
                self.nodes[node].is_strategic = True
                self.nodes[node].score_of_strategic = score
        self.turn_number = self.game.get_turn_number()['turn_number']

        for node_idx_str, owner_id in self.game.get_owners().items():
            self.nodes[int(node_idx_str)].owner = owner_id if owner_id != -1 else None

        for node_idx_str, troops_cnt in self.game.get_number_of_troops().items():
            self.nodes[int(node_idx_str)].number_of_troops = troops_cnt

        for node_idx_str, troops_cnt in self.game.get_number_of_fort_troops().items():
            node = self.nodes[int(node_idx_str)]
            if troops_cnt > 0:
                self.players_done_fort[node.owner] = True
            node.number_of_fort_troops = troops_cnt

        if self.stage == 0:
            self.remaining_init = [35, 35, 35]
            for node in self.nodes:
                if node.owner is None:
                    continue
                self.remaining_init[node.owner] -= node.number_of_troops
            self.temp_phase0_remains = copy.deepcopy(self.remaining_init)
        else:
            self.remaining_init = [0, 0, 0]
            self.remaining_init[self.player_id] = self.game.get_number_of_troops_to_put()['number_of_troops']
            # TODO calculate other players troop count

    def update_remaining_troops_by_map(self):
        n_nodes_belonging = defaultdict(lambda: 0)
        for node in self.nodes:
            if node.owner is None:
                continue
            n_nodes_belonging[node.owner] += 1
        n_strategic_belonging = defaultdict(lambda: 0)
        for node in self.nodes:
            if node.is_strategic and node.owner is not None:
                n_strategic_belonging[node.owner] += node.score_of_strategic
        for i in range(self.player_cnt):
            if i == self.player_id:
                continue
            self.later_added[i] = n_nodes_belonging[i] // 4 + n_strategic_belonging[i]
            self.remaining_init[i] = self.later_added[i]  # temporary substitute for remaining troops
            if self.phase_2_turns == 1 and self.player_id != 0:
                for j in range(1, 3):
                    p = (self.player_id + j) % 3
                    self.remaining_init[j] += self.temp_phase0_remains[j]
                    if p == 0:
                        break
            else:
                self.remaining_init[i] += 3

    def get_board_graph(self) -> nx.DiGraph:
        graph: nx.DiGraph = nx.DiGraph()

        for node in self.nodes:
            graph.add_node(node)
        for node in self.nodes:
            for nei in node.adj_main_map:
                graph.add_edge(node.id, nei.id)
        return graph

    def get_passable_board_graph(self, player) -> nx.DiGraph:
        graph: nx.DiGraph = nx.DiGraph()
        expected_casualty = get_expected_casualty()

        for node in self.nodes:
            graph.add_node(node.id)
        for node in self.nodes:
            for nei in node.adj_main_map:
                weight = 1 + expected_casualty[nei.number_of_troops + nei.number_of_fort_troops]
                if nei.owner in [player, None]:
                    continue
                graph.add_edge(node.id, nei.id, weight=weight)
        return graph

    def get_players_troop_gain(self) -> List[int]:
        player_troop_gains = [0, 0, 0]
        for node in self.nodes:
            if node.owner is None:
                continue
            player_troop_gains[node.owner] += node.score_of_strategic + 0.25
        for i in range(3):
            player_troop_gains[i] = int(player_troop_gains[i])
        return player_troop_gains

clients/utils/update_details.py
AICUP2023-Hybrid/Agent
# author: Mohamad Mahdi Reisi
# edit: Vahid Ghafourian
# Date: 2023/09/06
import io
# this is the main component of the game
# it includes all the players and nodes of the game and all the information about the game

# turn state:
## 1: add troops
## 2: attack
## 3: move troops

# game state:
## 1: still need to initialize the troops (initial state)
## 2: the game started (turns state)

import json
import math
import os

import PIL
import networkx as nx
from matplotlib import pyplot as plt
from matplotlib.lines import Line2D
from components.player import Player
from components.node import Node
from tools.calculate_number_of_troops import calculate_number_of_troops
import moviepy.editor as mp


class Game:
    def __init__(self) -> None:
        self.debug_logs = ""
        self.debug = False
        self.players = {}  # player_id: player object, includes all players in the game

        self.nodes = {}  # node_id: node object, includes all nodes of the map

        self.turn_number = 0  # each turn is a round for a player to play the game (it starts from 1)
        self.state = 1  # that could be 'add troops': 1, 'attack': 2, 'move troops': 3
        self.player_turn = None  # Player object: the player who is playing this turn
        self.game_started = False  # True if the game already started
        self.game_state = 1  # 1: still need to initialize the troops, 2: the game started
        self.config = None  # the config dictionary
        self.finish_func = None  # the function that will be called when the game is finished

        # the following variables are used to for log file
        self.log_initialize = []  # the log of the initialize phase
        self.log_node_owner = []  # the log of the node owner at the beginning of each turn
        self.log_troop_count = []  # the log of the number of troops at the beginning of each turn
        self.log_put_troop = []  # the log of the number of troops that are put on the map at each turn
        self.log_attack = []  # the log of the attacks at each turn
        self.log_fortify = {}  # the log of the move_troop at each turn
        self.log_fort = []  # the log of the fortify at each turn
        self.has_won_troop = False
        self.move_troop_done = False  # a boolean that shows if the move_troop is done or not in the current turn
        # the main log file that will saved at the end of the game
        self.log = {"initialize": self.log_initialize, "turns": {}}

        # variable for visualization
        self.G = nx.Graph()
        self.node_colors = []
        self.alpha = []
        self.node_shapes = []
        self.node_size = []
        self.strategic_node_list = []
        self.non_strategic_node_list = []
        self.pos = []
        self.flags = {0: "green", 1: "red", 2: "blue", None: "black"}
        self.pos = None
        self.labels = {}
        self.legend_elements = [
            Line2D([0], [0], marker='*', color='w', label='Winner: ??', markerfacecolor='gold', markersize=10),

            Line2D([0], [0], marker='>', color='w', label='Turn 0', markerfacecolor='black', markersize=10),
            Line2D([0], [0], marker='>', color='w', label='Round 0', markerfacecolor='black', markersize=10),

            Line2D([0], [0], marker='o', color='w', label='Player 1', markerfacecolor='red', markersize=15),
            Line2D([0], [0], marker='.', color='w', label='0', markerfacecolor='red', markersize=8),
            Line2D([0], [0], marker='X', color='w', label='0', markerfacecolor='red', markersize=8),
            Line2D([0], [0], marker='P', color='w', label='35', markerfacecolor='red', markersize=8),

            Line2D([0], [0], marker='o', color='w', label='Player 2', markerfacecolor='blue', markersize=15),
            Line2D([0], [0], marker='.', color='w', label='0', markerfacecolor='blue', markersize=8),
            Line2D([0], [0], marker='X', color='w', label='0', markerfacecolor='blue', markersize=8),
            Line2D([0], [0], marker='P', color='w', label='35', markerfacecolor='blue', markersize=8),

            Line2D([0], [0], marker='o', color='w', label='Player 3', markerfacecolor='green', markersize=15),
            Line2D([0], [0], marker='.', color='w', label='0', markerfacecolor='green', markersize=8),
            Line2D([0], [0], marker='X', color='w', label='0', markerfacecolor='green', markersize=8),
            Line2D([0], [0], marker='P', color='w', label='35', markerfacecolor='green', markersize=8),

            Line2D([0], [0], marker='o', color='w', label='Neutral', markerfacecolor='black', markersize=15)
        ]
        self.frames = []

    def update_game_state(self) -> None:
        # update the game state
        # this update will happen at the beginning of each turn

        # check if the the game is already in the turns state
        if self.game_state == 2:
            return

        # check if the players had enough turns to put all their initial troops
        if self.turn_number > int(self.config["number_of_players"]) * int(self.config["initial_troop"]):
            self.game_state = 2

    def add_player(self, player_id: int) -> None:
        # add a player to the game if it doesn't exist
        # this function will generate a new player object and add it to the players dictionary
        if player_id not in self.players:
            self.players[player_id] = Player(player_id)

    def read_map(self, map_file: str) -> None:
        # read the map from the json file and create the nodes and initialize them

        # open the json file and load it into a dictionary
        with open(map_file, 'r') as json_file:
            json_py = json.load(json_file)

            # create the nodes and add them to the nodes dictionary
        for id in range(json_py["number_of_nodes"]):
            node = Node(id)
            self.nodes[id] = node

        # add edges to the nodes
        for edge in json_py["list_of_edges"]:
            self.nodes[edge[0]].adj_main_map.append(self.nodes[edge[1]])
            self.nodes[edge[1]].adj_main_map.append(self.nodes[edge[0]])

        # add the strategic nodes to the nodes
        for id in json_py["strategic_nodes"]:
            self.nodes[id].is_strategic = True

        for i in range(len(json_py["scores_of_strategic_nodes"])):
            id = json_py["strategic_nodes"][i]
            score = json_py["scores_of_strategic_nodes"][i]
            self.nodes[id].score_of_strategic = score

    def check_all_players_ready(self) -> None:
        # this function will check if all players are ready to start the game
        # this function will be called after each player sends a ready request
        # this function also will start the game with a new thread if all players are ready

        # check if the game started before or not
        if self.game_started == True:
            return

        # check if all players were logged in
        if len(self.players) != self.config['number_of_players']:
            return

        # check if all players are ready
        for player in self.players.values():
            if not player.is_ready:
                return

        # start the game
        # change_turn(self)

        self.game_started = True

    def start_turn(self):
        # this function will be called at the beginning of each turn
        # it will initialize the turn variables in the game object

        # increase the turn number
        self.turn_number += 1

        # reset the move_troop_done variable
        self.move_troop_done = False

        # calculate the player who should play this turn
        player_id = self.turn_number % len(self.players)

        # check if the game needs to change state from initialize to turns
        self.update_game_state()

        # initialize the turn state to put troops for the player who should play this turn
        self.state = 1
        # initialize the player who should play this turn
        self.player_turn = self.players[player_id]
        self.has_won_troop = False
        # check if the game is in the turns state
        if self.game_state == 2:
            # calculate the number of troops that the player get at the beginning of this turn
            self.player_turn.number_of_troops_to_place += calculate_number_of_troops(player_id, self)
            # initialize the log variables
            # the log of the node owner at the beginning of each turn
            self.log_node_owner = [i.owner.id if i.owner is not None else -1 for i in self.nodes.values()]
            # the log of the number of troops at the beginning of each turn
            self.log_troop_count = [i.number_of_troops for i in self.nodes.values()]
            # clear the log of the number of troops that are put on the map at each turn
            self.log_put_troop = []
            # clear the log of the attacks at each turn
            self.log_attack = []
            # clear the log of the move_troop at each turn
            self.log_fortify = {}

        return player_id

    def end_turn(self):
        # this function will be called at the end of each turn
        # it will save all the log variables in the main log variable

        # check if the game is in the turns state
        if self.game_state == 2:
            self.log['turns']['turn' + str(self.turn_number)] = {
                "nodes_owner": self.log_node_owner,
                "troop_count": self.log_troop_count,
                "add_troop": self.log_put_troop,
                "attack": self.log_attack,
                "fortify": self.log_fortify,
                "fort": [i.number_of_fort_troops for i in self.nodes.values()]
            }

    def print(self, text):
        # this function will print the text in the a log
        self.debug_logs += text + "\n"

    def add_node_to_player(self, node_id, player_id):
        self.players[player_id].nodes.append(self.nodes[node_id])
        self.nodes[node_id].owner = self.players[player_id]

    def remove_node_from_player(self, node_id, player_id):
        self.players[player_id].nodes.remove(self.nodes[node_id])
        self.nodes[node_id].owner = None

    def initialize_visualization(self, c_one, c_two, c_three):
        plt.figure(figsize=(12, 12))
        self.node_colors = [self.config['normal_node_color']] * len(self.nodes)
        self.alpha = [self.config['normal_node_alpha']] * len(self.nodes)
        self.node_shapes = [self.config['normal_node_shape']] * len(self.nodes)
        self.node_size = [self.config['normal_node_size']] * len(self.nodes)

        self.legend_elements[3].set_label(f'{c_one if isinstance(c_one, str) else c_one.__name__()}')
        self.legend_elements[7].set_label(f'{c_two if isinstance(c_two, str) else c_two.__name__()}')
        self.legend_elements[11].set_label(f'{c_three if isinstance(c_three, str) else c_three.__name__()}')
        for i in range(len(self.nodes)):
            self.G.add_node(i)
        for _, u in self.nodes.items():
            if u.is_strategic:
                self.node_shapes[u.id] = self.config['strategic_node_shape']
                self.node_size[u.id] = self.config['strategic_node_size']
                self.alpha[u.id] = self.config['strategic_node_alpha']
            for v in u.adj_main_map:
                self.G.add_edge(u.id, v.id)
        self.strategic_node_list = [i for i in range(self.G.number_of_nodes()) if
                                    self.node_shapes[i] == self.config['strategic_node_shape']]
        self.non_strategic_node_list = [i for i in range(self.G.number_of_nodes()) if
                                        self.node_shapes[i] == self.config['normal_node_shape']]
        self.pos = nx.spring_layout(self.G,
                                    k=2. * 1 / math.sqrt(self.G.number_of_nodes()),
                                    iterations=100,
                                    seed=69)

    def update_visualization(self):
        for _, u in self.nodes.items():
            if u.owner is not None:
                self.node_colors[u.id] = self.flags[u.owner.id]
                self.labels[u.id] = u.number_of_troops + u.number_of_fort_troops

    def visualize(self, is_finished, index, end_type):
        self.update_visualization()
        for shape in [self.config['normal_node_shape'], self.config['strategic_node_shape']]:
            nodelist = [i for i in range(len(self.node_shapes)) if self.node_shapes[i] == shape]
            nx.draw_networkx_nodes(
                self.G, self.pos,
                nodelist=nodelist,
                node_size=[self.node_size[i] for i in nodelist],
                node_shape=shape,
                node_color=[self.node_colors[i] for i in nodelist],
                alpha=[self.alpha[i] for i in nodelist],
            )
        attacks_node_set = [{attack['attacker'], attack['target']} for attack in self.log_attack]
        player_color = self.flags[self.player_turn.id]
        nx.draw_networkx_edges(
            self.G, self.pos,
            edge_color=[player_color if {a, b} in attacks_node_set else 'k' for a, b in self.G.edges],
            width=[2. if {u, v} in attacks_node_set else 1. for u, v in self.G.edges]
        )
        edge_labels = {}
        for u, v in self.G.edges:
            for i, attack in enumerate(attacks_node_set):
                if {u, v} == attack:
                    details = self.log_attack[i]
                    attacker_troops = details['new_troop_count_attacker']
                    defender_troops = details['new_troop_count_target']
                    won = False
                    if details['new_target_owner'] == self.player_turn.id:
                        won = True
                    edge_labels[(u, v)] = f'{"W" if won else "L"}{attacker_troops}-{defender_troops}'
        nx.draw_networkx_edge_labels(
            self.G, self.pos, edge_labels=edge_labels, font_color=player_color,
            font_size=10
        )

        # Turn, Round, and Winner index
        turn_label = ""
        round_label = ""
        if self.game_state == 1:
            turn_label = f'Turn {self.turn_number}'
            round_label = f'Round {math.ceil(self.turn_number / 3)}'
        elif self.game_state == 2:
            turn_label = f'Turn {self.turn_number} ({self.turn_number - 105})'
            round_label = f'Round {math.ceil(self.turn_number / 3)} ({math.ceil((self.turn_number - 105) / 3)})'
        self.legend_elements[1].set_label(turn_label)
        self.legend_elements[2].set_label(round_label)
        if is_finished:
            self.legend_elements[0].set_label(f'Winner: P{index}')

        # Current all troops on map
        normal_troops_player_0 = self.players[0].get_normal_troops()
        normal_troops_player_1 = self.players[1].get_normal_troops()
        normal_troops_player_2 = self.players[2].get_normal_troops()
        fort_troops_player_0 = self.players[0].get_fort_troops()
        fort_troops_player_1 = self.players[1].get_fort_troops()
        fort_troops_player_2 = self.players[2].get_fort_troops()
        self.legend_elements[4].set_label(f"{normal_troops_player_1} + {fort_troops_player_1}")
        self.legend_elements[8].set_label(f"{normal_troops_player_2} + {fort_troops_player_2}")
        self.legend_elements[12].set_label(f"{normal_troops_player_0} + {fort_troops_player_0}")

        # Points gain
        normal_point_player_0 = self.players[0].get_normal_point()
        normal_point_player_1 = self.players[1].get_normal_point()
        normal_point_player_2 = self.players[2].get_normal_point()
        strategical_point_player_0 = self.players[0].get_strat_point()
        strategical_point_player_1 = self.players[1].get_strat_point()
        strategical_point_player_2 = self.players[2].get_strat_point()
        label_1 = (f"{strategical_point_player_1} + {normal_point_player_1} = "
                   f"{strategical_point_player_1 + normal_point_player_1}")
        label_2 = (f"{strategical_point_player_2} + {normal_point_player_2} = "
                   f"{strategical_point_player_2 + normal_point_player_2}")
        label_3 = (f"{strategical_point_player_0} + {normal_point_player_0} = "
                   f"{strategical_point_player_0 + normal_point_player_0}")
        self.legend_elements[5].set_label(label_1)
        self.legend_elements[9].set_label(label_2)
        self.legend_elements[13].set_label(label_3)

        # Troops to put
        self.legend_elements[6].set_label(f"{self.players[1].number_of_troops_to_place}")
        self.legend_elements[10].set_label(f"{self.players[2].number_of_troops_to_place}")
        self.legend_elements[14].set_label(f"{self.players[0].number_of_troops_to_place}")

        plt.legend(handles=self.legend_elements,
                   bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
        nx.draw_networkx_labels(
            self.G, self.pos, self.labels,
            font_size=15,
            font_color="white"
        )
        fig = plt.gcf()
        fig.tight_layout()
        fig.canvas.draw()
        img = PIL.Image.frombytes('RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
        self.frames.append(img)
        plt.clf()

    def save_gif(self, file_name):
        if not os.path.exists(self.config["visualization_folder"]):
            os.mkdir(self.config["visualization_folder"])
        # self.frames[-1].save(f'{self.config["visualization_folder"]}/{file_name}-ending.png')
        self.frames[0].save(
            f'{self.config["visualization_folder"]}/{file_name}.gif',
            save_all=True,
            append_images=self.frames[1:] + self.frames[:1],  # append rest of the images
            duration=[10000 if i >= len(self.frames) - 1 else 3000 for i in range(len(self.frames) + 1)],
            # in milliseconds
            loop=0
        )

    def save_mp4(self, file_name):
        clip = mp.VideoFileClip(f'{self.config["visualization_folder"]}/{file_name}.gif')
        clip.write_videofile(f'{self.config["visualization_folder"]}/{file_name}.mp4')
        os.remove(f'{self.config["visualization_folder"]}/{file_name}.gif')

components/game.py
AICUP2023-Hybrid/Agent
# author: Mohamad Mahdi Reisi
import copy
from typing import List


# this is the most basic component of the Map
# each region is a node, and each node has a list of adjacent nodes in the main map
# each node has an owner, which is a player object.


class Node:
    def __init__(self, id) -> None:
        self.saved = []
        self.id = id  # each node has an id that is unique in the game
        self.owner = None  # Player object that owns this node
        self.number_of_troops = 0  # number of troops that are placed on this node
        self.number_of_fort_troops = 0  # number of fort troops that are placed on this node
        self.adj_main_map: List[Node] = []  # list of Node objects that are adjacent to this node in the main map
        self.is_strategic = False  # if this node is strategic or not
        self.score_of_strategic = 0  # the score of this node if it's strategic

    def save_version(self):
        self.saved.append([self.owner, self.number_of_troops, self.number_of_fort_troops])

    def restore_version(self):
        if len(self.saved) > 0:
            self.owner, self.number_of_troops, self.number_of_fort_troops = self.saved.pop()

components/node.py
AICUP2023-Hybrid/Agent
# author: Mohamad Mahdi Reisi
import math


# this is one of the most basic components of the Risk game.

class Player:
    def __init__(self, id) -> None:
        self.nodes = []  # list of Node objects that owned by this player
        self.id = id  # each player has an id that is unique in the game
        self.number_of_troops_to_place = 0  # number of troops that the player have but not placed on the map
        self.port = ""  # the port that the player should run a server on it to listen to the requests
        self.ip = ""  # the ip of the player (it used to send requests to the player)
        self.is_ready = False  # a boolean that shows if the player is ready to get requests and play the game or not
        self.token = ''  # a token that is used to show the player that requests are from the game server
        self.use_fort = False  # a boolean that shows if the player used fortify or not

    def get_fort_troops(self):
        return sum([node.number_of_fort_troops for node in self.nodes])

    def get_normal_troops(self):
        return sum([node.number_of_troops for node in self.nodes])

    def get_normal_point(self):
        return math.floor(sum([1 for node in self.nodes]) / 4)

    def get_strat_point(self):
        return sum([node.score_of_strategic for node in self.nodes if node.is_strategic])

components/player.py
AICUP2023-Hybrid/Agent
# author: Vahid Ghafourian
# Date: 2023/09/06
import math
import random
from multiprocessing import Pool
from typing import List, Any
from tqdm.contrib.concurrent import process_map
from Kernel import Kernel
from clients.salar.SalarAgent import ClientSalar
from components.game import Game
from clients.client_ai import ClientAi
import networkx as nx
import matplotlib.pyplot as plt
from clients.client_enemy_one import ClientEnemyOne
from clients.client_enemy_two import ClientEnemyTwo
from turn_controllers import change_turn
from collections import defaultdict
import sys, os
from tqdm import tqdm

import json


def read_config():
    with open('config.json', 'r') as f:
        config = json.load(f)
    return config


# Disable
def block_print():
    sys.stdout = open(os.devnull, 'w')


# Restore
def enable_print():
    sys.stdout = sys.__stdout__


def get_clients(kernel, player_turn: int) -> List[Any]:
    # Build Enemy clients
    clients = []
    for i in range(3):
        if player_turn == i:
            clients.append(ClientAi(kernel))
        else:
            clients.append(ClientEnemyTwo(kernel))
    return clients


def get_clients_random(kernel, *args, **kwargs):
    clients = list(enumerate([ClientAi, ClientAi, ClientEnemyTwo]))

    random.shuffle(clients)
    res = [client(kernel, i) for i, client in clients]
    kernel.main_game.team_names = [client.__name__() for client in res]
    return res


def get_kernel():
    # ask player to choose map from the list of maps
    maps = os.listdir('maps')

    kernel_main_game = Game()
    # get the selected map from the player
    selected_map = '3'  # str(random.choice(list(range(1, 5))))

    while selected_map.isdigit() is False or int(selected_map) >= len(maps) or int(selected_map) < 0:
        # show the list of maps from the maps folder
        print("Choose a map from the list of maps:")
        for i, map in enumerate(maps):
            print(i, '-', map)
        selected_map = input("Enter the number of the map you want to choose: ")

    # read the selected map
    kernel_main_game.read_map('maps/' + maps[int(selected_map)])

    # read config
    kernel_config = read_config()

    # Build Kernel
    return Kernel(kernel_main_game, kernel_config)


def get_score_by_client(score: dict):
    return "\n".join([f"{k}: {v}" for k, v in sorted(score.items(), key=lambda x: x[0])])


def do(i):
    visualize = False
    # block_print()
    # Creating and running game
    kernel = get_kernel()
    clients = get_clients_random(kernel)
    kernel.main_game.game_id = i
    wp, end_type = run_game(kernel, clients, game_vis_file_name=f'game{i}' if visualize else None)

    enable_print()
    if visualize:
        print(f'game result {i}: {clients[wp].__name__()}')
    return wp, end_type, [client.__name__() for client in clients]


def main():
    n_iterations = 100
    wins = defaultdict(lambda: 0)
    total_by_pos = defaultdict(lambda: [0, 0, 0])
    wins_by_pos = defaultdict(lambda: [0, 0, 0])
    by_score = defaultdict(lambda: [0, 0, 0])
    by_strategic = defaultdict(lambda: [0, 0, 0])
    losses_list = []
    for wp, end_type, client_names in [do(0)]:  # process_map(do, range(n_iterations), max_workers=8):
        wins[client_names[wp]] += 1
        wins_by_pos[client_names[wp]][wp] += 1
        if end_type == 'by_score':
            by_score[client_names[wp]][wp] += 1
        else:
            by_strategic[client_names[wp]][wp] += 1
        for pos, client_name in enumerate(client_names):
            total_by_pos[client_name][pos] += 1

    print(f'lost games: {losses_list}')
    print(f'---wins---\n{get_score_by_client(wins)}')
    print('---by player results---')
    client_names = sorted([item[0] for item in total_by_pos.items()])
    win_percentage_by_pos = {
        name: [round(wins_by_pos[name][i] / max(1, total_by_pos[name][i]), ndigits=2) for i in range(3)]
        for name in client_names
    }
    print("{:<15} {:<20} {:<20} {:<20}".format(*([''] + client_names)))
    for row_name, dct in [
        ('wins', wins_by_pos), ('by_score', by_score),
        ('by_strategic', by_strategic), ('total', total_by_pos),
        ('win_percentage', win_percentage_by_pos)
    ]:
        print("{:<15} {:<20} {:<20} {:<20}".format(
            *([row_name] + [str(dct[name]) for name in client_names])
        ))


def run_game(kernel, clients, game_vis_file_name=None):
    winning_player, end_type = change_turn(kernel.main_game, *clients,
                                           visualize=game_vis_file_name is not None)
    if game_vis_file_name:
        kernel.main_game.save_gif(game_vis_file_name)
        kernel.main_game.save_mp4(game_vis_file_name)
    return winning_player, end_type


if __name__ == '__main__':
    main()

offline_main.py
AICUP2023-Hybrid/Agent
import random

from clients.stages.attack import plan_attack
from clients.stages.initializer import initialize_turn
from online_src.game import Game

flag = False


def initializer(game: Game):
    initialize_turn(game)
    game.next_state()


def turn(game):
    game.game_data.stage = 1
    plan_attack(game)
    game.game_data.phase_2_turns += 1

online_main.py
AICUP2023-Hybrid/Agent
import requests


class Game:
    def __init__(self, token, server_ip, server_port) -> None:
        self.token = token
        self.server_ip = server_ip
        self.server_port = server_port
        self.my_turn = False
        from clients.utils.update_details import GameData
        self.game_data: GameData = GameData(self)

    def handel_output(self, response):
        code = response.status_code
        if 200 <= code < 300:
            return eval(response.text)
        if 'error' in response.json():
            print(response.json()['error'])
            raise Exception(response.json()['error'])
        else:
            print("unknown error")
            raise Exception("unknown error")

    def get_owners(self):
        """
            returns a dictionary of node_id: owner_id
            node_id: str
            owner_id: int
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_owners',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def get_number_of_troops(self):
        """
            returns a dictionary of node_id: number_of_troops
            node_id: str
            number_of_troops: int
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_troops_count',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def get_state(self):
        """
            returns a dictionary containing the state of the game 
            1: put_troop
            2: attack
            3: move_troop
            4: fort 
            {'state': number_of_state}
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_state',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return

        return self.handel_output(resp)

    def get_turn_number(self):
        """
            returns a dictionary containing the turn number
            {'turn_number': number_of_turn}
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_turn_number',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def get_adj(self):
        """
            return the adjacent nodes of each node
            returns a dictionary of node_id: [adjacent_nodes]
            node_id: str
            adjacent_nodes: list of int
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_adj',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def next_state(self):
        """
            changes the state of the turn to the next state
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/next_state',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def put_one_troop(self, node_id):
        """
            puts one troop in the node with the given id 
            this function can only be used in the put_troop state in the initialize function
        """
        body = {
            'node_id': node_id
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/put_one_troop',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

    def put_troop(self, node_id, num):
        """
            puts num troops in the node with the given id
            this function can only be used in the put_troop state in the turn function
        """
        body = {
            'node_id': node_id,
            'number_of_troops': num
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/put_troop',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

    def get_player_id(self):
        """
            returns the id of the player
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_player_id',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)['player_id']

    def attack(self, attacking_id, target_id, fraction, move_fraction):
        """
            attacks the target node with the given fraction of troops
        """
        body = {
            'attacking_id': attacking_id,
            'target_id': target_id,
            'fraction': fraction,
            'move_fraction': move_fraction
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/attack',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

    def move_troop(self, source, destination, troop_count):
        """
            moves the given number of troops from the source node to the destination node
        """
        body = {
            'source': source,
            'destination': destination,
            'troop_count': troop_count
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/move_troop',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

    def get_strategic_nodes(self):
        """
            returns a list of strategic nodes and their score
            {"strategic_nodes": [node_id, ...], "score": [score, ...]}
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_strategic_nodes',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def get_number_of_troops_to_put(self):
        """
            returns the number of troops that the player can put in the put_troop state
            {"number_of_troops": number_of_troops}
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_number_of_troops_to_put',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def get_reachable(self, node_id):
        """
            returns a dictionary of "reachable" key and a list of reachable nodes
            {"reachable": [node_id, ...]}
        """
        body = {
            'node_id': node_id
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/get_reachable',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

    def get_number_of_fort_troops(self):
        """
            returns the number of troops that used to defend the node
            {node_id: number_of_troops, ...}
            node_id: str
            number_of_troops: int
        """
        try:
            resp = requests.request('GET', f'http://{self.server_ip}:{self.server_port}/get_number_of_fort_troops',
                                    headers={'x-access-token': self.token})
        except:
            print("can't make request")
            return
        return self.handel_output(resp)

    def fort(self, node_id, troop_count):
        """
            fortifies the node with the given number of troops
        """
        body = {
            'node_id': node_id,
            'troop_count': troop_count
        }
        try:
            resp = requests.request('POST', f'http://{self.server_ip}:{self.server_port}/fort',
                                    headers={'x-access-token': self.token}, data=body)
        except:
            print("can't make request")
            return {}
        return self.handel_output(resp)

online_src/game.py
AICUP2023-Hybrid/Agent
import json
import requests
from flask import Flask
import random
from online_src.game import Game
from flask import request
from functools import wraps
from flask import jsonify
from online_main import turn as player_turn
from online_main import initializer as player_initializer
import threading
import os
import logging

# read config file from config.json
config = json.load(open('config.json'))

server_ip = config['server_ip']
server_port = config['server_port']

# disable proxy and vpn 
os.environ['NO_PROXY'] = f'{server_ip}'

# make a login request to the server 
try:
    # generate a random password 
    password = int(random.randint(100000, 999999))

    # make a dictionary to send to the server
    login_data = {'token': password}

    # send login data as a form in a POST request
    login_response = requests.request('POST', f'http://{server_ip}:{server_port}/login', data=login_data).json()
except:
    print("the server is not running")
    exit()

try:
    id = login_response['player_id']
    token = login_response['token']
    my_port = login_response['port']
except:
    print('there is a problem in the server response')
    try:
        print(login_response['error'])
    except:
        print("there is no error message in the response")
    exit()

# generate game object
game = Game(token, server_ip, server_port)


# a function to check the password in the x-access-token header
def token_required(func):
    """
    This function is used as a decorator to check the token
    """

    # use wraps to keep the original function name
    @wraps(func)
    def decorator():
        token = None
        output_dict = dict()

        # ensure the jwt-token is passed with the headers
        if 'x-access-token' in request.headers:
            token = request.headers['x-access-token']
        if not token:  # throw error if no token provided
            output_dict['error'] = 'Token is missing!'
            return jsonify(output_dict), 401

        token = int(token)
        # check if the token is correct
        if token != password:
            output_dict['error'] = 'Invalid token!'
            return jsonify(output_dict), 401

        return func()

    return decorator


# make a server to get the start of my turn
app = Flask(__name__)
turn_thread = None

app.logger.propagate = False
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)


@app.route('/init', methods=['GET'])
@token_required
def initializer():
    global turn_thread
    game.my_turn = True
    print('initializer started')
    turn_thread = threading.Thread(target=player_initializer, args=(game,))
    turn_thread.start()
    return 'ok'


@app.route('/turn', methods=['GET'])
@token_required
def turn():
    global turn_thread
    game.my_turn = True
    print('turn started')
    turn_thread = threading.Thread(target=player_turn, args=(game,))
    turn_thread.start()
    return 'ok'


@app.route('/end', methods=['GET'])
@token_required
def end_turn():
    print('turn ended')
    game.my_turn = False
    return 'ok'


@app.route('/kill', methods=['GET'])
@token_required
def shutdown():
    print('kill')
    own_pid = os.getpid()
    os.kill(own_pid, 9)
    return 'ok'


def ready():
    resp = requests.request('GET', f'http://{server_ip}:{server_port}/ready', headers={'x-access-token': token})
    code = resp.status_code
    if 200 <= code < 300:
        print('ready')
    else:
        print(resp.json()['error'])
        print("can't make a ready request")
        exit()


with app.app_context():
    ready()

app.run(debug=True, port=my_port, use_reloader=False, host=config['host'])

online_src/initialize.py
AICUP2023-Hybrid/Agent
import os

file_path = os.path.abspath(__file__).split('run.py')[0]

# go to the file path address
os.chdir(file_path)

from online_src.initialize import *


run.py
AICUP2023-Hybrid/Agent
def calculate_number_of_troops(player_id, main_game):
    # calculate the number of troops to give a player at the beginning of a turn
    number_of_nodes = len(main_game.player_turn.nodes)
    score_of_strategic_nodes = sum([i.score_of_strategic for i in main_game.player_turn.nodes if i.is_strategic])
    return max(number_of_nodes // 4 + score_of_strategic_nodes, main_game.config['minimum_troops_per_turn'])

tools/calculate_number_of_troops.py
AICUP2023-Hybrid/Agent
mark = []

def DFS(u, v, main_game, player_id):
    global mark
    mark[u] = 1
    if u == v:
        return True, [v]
    path = []
    for node in main_game.nodes[u].adj_main_map:
        if mark[node.id] == 0 and node.owner != None and node.owner.id == player_id:
            res, path = DFS(node.id, v, main_game, player_id)
            if res:
                path = [u] + path
                return True, path
    return False, []
                


def find_path(u, v, main_game, player_id):
    global mark
    # find a path from node u to node v that all the nodes in the path are owned by the player
    # return the path as a list of nodes
    # if there is no path return None
    mark = [0 for i in range(len(main_game.nodes))]
    return DFS(u, v, main_game, player_id)
    

    


tools/find_path.py
AICUP2023-Hybrid/Agent
ans = []
mark = []

def DFS(node_id, main_game, player_id):
    global ans
    global mark
    mark[node_id] = 1
    ans.append(node_id)
    for node in main_game.nodes[node_id].adj_main_map:
        if mark[node.id] == 0 and node.owner != None and node.owner.id == player_id:
            DFS(node.id, main_game, player_id)


def find_reachable(node_id, main_game):
    global ans
    global mark

    mark = [0 for i in range(len(main_game.nodes))]
    ans.clear()
    if main_game.nodes[node_id].owner == None:
        return ans
    DFS(node_id, main_game, main_game.nodes[node_id].owner.id)
    return ans 
tools/find_reachable.py
AICUP2023-Hybrid/Agent
# author: Mohamad Mahdi Reisi
# Date: 2023/8/16

# Edit: Vahid Ghafourian
# Date: 2023/09/06

import json
import datetime
import os
from time import time


def calculate_score(main_game):
    # (number of nodes * 1000) + (number of troops)+ (3000/score of strategic nodes)
    scores = []
    for i in main_game.players.values():
        number_of_nodes = len(i.nodes)
        number_of_troops = 0
        strategic_score = 0
        for j in i.nodes:
            number_of_troops += j.number_of_troops
            if j.is_strategic:
                strategic_score += 3000 // j.score_of_strategic

        scores.append((number_of_nodes * 1000) + number_of_troops + strategic_score)
    return scores


def check_finish(main_game):
    # check if the game pass the minimum number of turns
    if main_game.turn_number < int(main_game.config["minimum_number_of_turns"]):
        return False, None, ''

    # find the number of strategic nodes for each player
    players_strategic_nodes_count = []
    for player in main_game.players.values():
        number_of_strategic_nodes = 0
        for node in player.nodes:
            if node.is_strategic:
                number_of_strategic_nodes += 1
        players_strategic_nodes_count.append(number_of_strategic_nodes)

    # check if there is a player with enough strategic nodes to win
    for i in range(len(players_strategic_nodes_count)):
        if players_strategic_nodes_count[i] >= int(main_game.config["number_of_strategic_nodes_to_win"]):
            if main_game.debug:
                main_game.print("player won because of having enough strategic nodes")
            print("Player ", str(i), " won because of having enough strategic nodes.")
            scores = calculate_score(main_game)
            scores[i] += sum(scores)
            game_finished(main_game, scores)
            return True, int(str(i)), 'by_strategic'
    # check if the game is finished
    if main_game.turn_number >= int(main_game.config["number_of_turns"]):
        if main_game.debug:
            main_game.print("game finished because of number of turns")
        scores = calculate_score(main_game)
        print("The game ended due to the completion of the number of turns and player", scores.index(max(scores)),
              "won with a score of:", max(scores))
        game_finished(main_game, scores)
        return True, scores.index(max(scores)), 'by_score'
    return False, None, ''


def game_finished(main_game, score):
    # finish the game
    # make log folder if it does not exist
    if not os.path.exists("log"):
        os.makedirs("log")

    # add score the the log file
    main_game.log["score"] = score
    main_game.log["team_names"] = main_game.team_names

    # generate and save the main_game.log file into a json file in the log folder
    with open(f"log/{main_game.game_id}.json", "w") as log_file:
        json.dump(main_game.log, log_file)

    # generate and save an export from main_game object and save in the result log folder
    # generate export
    export = dict()
    export['node_owners'] = [i.owner.id if i.owner != None else -1 for i in main_game.nodes.values()]
    export['troop_count'] = [i.number_of_troops for i in main_game.nodes.values()]
    export['turn_number'] = main_game.turn_number
    export['score'] = score
    # add the number of strategic nodes for each player
    for player in main_game.players.values():
        export['player' + str(player.id) + " strategic nodes"] = len([i for i in player.nodes if i.is_strategic])

        # make result_log folder if it does not exist
    if not os.path.exists("result_log"):
        os.makedirs("result_log")

    # save the export in the result log folder
    with open("result_log/" + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".json", "w") as result_log_file:
        json.dump(export, result_log_file)

    print(export)
    # write debug_logs in the text file in the debug_log folder
    if main_game.debug:
        # make debug_log folder if it does not exist
        if not os.path.exists("debug_log"):
            os.makedirs("debug_log")
        with open("debug_log/" + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".txt", "w") as debug_log_file:
            debug_log_file.write(main_game.debug_logs)


def change_turn(main_game, c_ai, c_two, c_three, directory = "tmp", visualize=False):
    if visualize:
        main_game.initialize_visualization(c_two,c_three,c_ai)
    while True:
        # increase the turn number and initialize the turn
        start_time = time()
        player_id = main_game.start_turn()

        # add the turn number to the logs
        if main_game.debug:
            # print("start turn:", main_game.turn_number)
            main_game.print("----------------------------- start turn: " + str(
                main_game.turn_number) + "----------------------------")
            main_game.print(
                "player: " + str(player_id) + ' -- start time ' + datetime.datetime.now().strftime("%H:%M:%S"))
            # print the owner and number of troops of each node at the beginning of the turn
            for i in main_game.nodes.values():
                main_game.print(
                    f"node {i.id}: owner: {i.owner.id if i.owner is not None else -1}, number of troops: {i.number_of_troops} , number of fort troops: {i.number_of_fort_troops}")

        # show number of troops that the player did not put on the map
        if main_game.debug:
            main_game.print(
                f"player {player_id} has {main_game.player_turn.number_of_troops_to_place} troops to put on the map")

        print("Turn Number:", main_game.turn_number, ' =' * 20)
        # wait for the player to play
        if main_game.game_state == 2:
            if player_id == 0:
                c_ai.turn()
            elif player_id == 1:
                c_two.turn()
            elif player_id == 2:
                c_three.turn()
            else:
                print('wrong id:' + str(player_id))
                input()
        elif main_game.game_state == 1:
            if player_id == 0:
                c_ai.initializer_turn()
            elif player_id == 1:
                c_two.initializer_turn()
            elif player_id == 2:
                c_three.initializer_turn()
            else:
                print('wrong id:' + str(player_id))
                input()
        # end the turn to add the logs for client
        end_time = time()
        main_game.end_turn()

        print(f'time elapsed: {end_time - start_time}')
        if main_game.debug:
            main_game.print("end turn: " + datetime.datetime.now().strftime("%H:%M:%S"))
        # check if the game is finished
        is_finished, index, end_type = check_finish(main_game)
        if visualize:
            main_game.visualize(is_finished, index, end_type)
        if is_finished:
            return index, end_type

turn_controllers.py
AICUP2023-Hybrid/Agent
import json

import numpy as np
import requests
import sys

from components.game import Game
from offline_main import read_config

responses = []
if sys.argv[1] == '-f':
    responses = [('local', 'map4.json', json.load(open(sys.argv[2])))]
elif sys.argv[1] == '-g':
    game_id = int(sys.argv[2])
    api_url = 'https://api.aicup2023.ir/api/match/'
    auth = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjk2MDYwNTMxLCJpYXQiOjE2OTU5NzQxMzEsImp0aSI6IjRlYTM2NTQ5NjEzZjQ1M2E5OTcwNGNmOTk3N2UyYzZjIiwidXNlcl9pZCI6MTIzNH0.D511skEYT8VGnsgNqE9iX4ATlH3LqOy2qEsnR2bZQOs'
    games = requests.get(api_url, headers={'Authorization': auth}).json()
    game = games[len(games) - game_id]
    matches = game['sub_matches']
    for i, match in enumerate(matches):
        mx_score = max(match[f'team{i}_score'] for i in range(1, 4))
        my_score = [match[f'team{i}_score'] for i in range(1, 4) if match[f'team{i}'] == 'Hybrid'][0]
        s = 'loss'
        print(mx_score, my_score)
        if mx_score == my_score:
            s = 'win'
        log_url = match['game_log']
        responses.append((f'{game_id}-{i + 1}-{s}', game['map_name'], requests.get(log_url).json()))
else:
    log_url = sys.argv[1]
    map = sys.argv[2]
    responses = [('spec', map, requests.get(log_url).json())]


for game_id, map, response in responses:
    game = Game()
    game.read_map(f'./maps/{map}')
    game.config = read_config()

    for i in range(3):
        game.add_player(i)
        game.players[i].number_of_troops_to_place = game.config['initial_troop']
        game.player_id = i

    init_data = response['initialize']
    turns_data = sorted(response['turns'].items())
    game.initialize_visualization(*[response['team_names'][i] for i in [1, 2, 0]])
    winner = np.argmax(response['score'])

    last_ind = 0
    for _ in range(1, 106):
        player_id = game.start_turn()
        player = game.players[player_id]
        if last_ind < len(init_data) and player_id == init_data[last_ind][0]:
            node = game.nodes[init_data[last_ind][1]]
            node.number_of_troops += 1
            player.number_of_troops_to_place -= 1
            node.owner = player
            if node not in player.nodes:
                player.nodes.append(node)
            last_ind += 1
        game.end_turn()
        # game.visualize(False, None, None)

    for ti, (turn, data) in enumerate(turns_data):
        owners = data['nodes_owner']
        troops = data['troop_count']
        for _, player in game.players.items():
            player.nodes = []
        for node_id in range(len(owners)):
            node = game.nodes[node_id]
            if owners[node_id] != -1:
                node.owner = game.players[owners[node_id]]
                game.players[node.owner.id].nodes.append(node)
            node.number_of_troops = troops[node_id]

        player_id = game.start_turn()
        player = game.players[player_id]
        for item in data['add_troop']:
            node_id, troop_cnt = item
            player.number_of_troops_to_place -= troop_cnt
            game.nodes[node_id].number_of_troops += troop_cnt
            game.nodes[node_id].owner = player
            if game.nodes[node_id] not in player.nodes:
                player.nodes.append(game.nodes[node_id])

        game.log_attack = data['attack']
        game.end_turn()
        game.visualize(ti == len(turns_data) - 1, winner, None)

        forts = data['fort']
        for node_id in range(len(owners)):
            node = game.nodes[node_id]
            node.number_of_fort_troops = forts[node_id]

        for item in data['attack']:
            target = item['target']
            new_owner = item['new_target_owner']
            if new_owner == player_id:
                player.number_of_troops_to_place += read_config()['number_of_troops_after_successful_attack']
                break

    game.save_gif(f'{game_id}')
    game.save_mp4(f'{game_id}')



visualize_log.py
AICUP2023-Hybrid/Agent