diff --git a/ai/README.md b/ai/README.md index d089b92c..fb966d8f 100644 --- a/ai/README.md +++ b/ai/README.md @@ -83,7 +83,7 @@ make zappy_ai_re 4. Run the AI module with the following command: ```bash -./zappy_ai_re -p -n -h +./zappy_ai -p -n -h ``` Replace ``, ``, and `` with the appropriate values for your game server. @@ -91,10 +91,9 @@ Replace ``, ``, and `` with the appropriate values for you You can also run the AI module with logs enabled using the `-l` flag: ```bash -./zappy_ai_re -p -n -h -l on +./zappy_ai -p -n -h -l on ``` - ## Testing To test the AI module, you can run the unit tests provided in the `tests` directory. To run the tests, use the following command at the root of the project: diff --git a/bonus/ai-controller/Makefile b/bonus/ai-controller/Makefile new file mode 100644 index 00000000..a5f0a173 --- /dev/null +++ b/bonus/ai-controller/Makefile @@ -0,0 +1,40 @@ +## +## EPITECH PROJECT, 2024 +## AI Zappy +## File description: +## AI Makefile +## + +NAME = zappy_ai + +TEST_NAME = zappy_ai_tests + +all: $(NAME) + +$(NAME): + cp src/main.py $(NAME) + chmod 775 $(NAME) + cp $(NAME) ../ + +clean: + rm -rf __pycache__ + +fclean: clean + rm -f $(NAME) + rm -f ../$(NAME) + rm -f $(TEST_NAME) + rm -f ../$(TEST_NAME) + +re: fclean all + +install-deps: + sudo dnf install python3-virtualenv -y + virtualenv zappy_ai_env + ./zappy_ai_env/bin/pip install -r requirements.txt + +tests_run: + cp ../tests/ai/tests/MainTest.py $(TEST_NAME) + chmod 775 $(TEST_NAME) + cp $(TEST_NAME) ../ + ./zappy_ai_env/bin/coverage run ../$(TEST_NAME) + ./zappy_ai_env/bin/coverage report diff --git a/bonus/ai-controller/README.md b/bonus/ai-controller/README.md new file mode 100644 index 00000000..c1d1994e --- /dev/null +++ b/bonus/ai-controller/README.md @@ -0,0 +1,106 @@ +# Zappy Project - AI Controller Bonus + +## Overview + +Welcome to the Zappy project for the second year at Epitech. This project involves developing a multiplayer network game, including a server, clients, and intelligent bots. As a bonus, we have implemented a custom controller that allows you to take control of the AI and play the game directly using Pygame. This feature adds a new dimension of interactivity and allows for more immersive game testing. + +## Features + +The AI controller provides the following features: + +1. **Game Control**: Play the game directly using Pygame. + +2. **AI Control**: Take control of the AI and play the game as an AI agent. + +3. **Game Monitoring**: Monitor the game state and AI interactions in real-time (through the GUI). + +## Installation + +To install the AI controller, follow these steps: + +1. Clone the repository: + +```bash +git clone https://github.com/FppEpitech/Zappy +``` + +2. Navigate to the `bonus/ai-controller` directory: + +```bash +cd Zappy/bonus/ai-controller +``` + +3. Install the prerequisites: + +Ubuntu: + +```bash +sudo apt-get install python3 python3-pip virtualenv +``` + +Fedora: + +```bash +sudo dnf install python3 python3-pip virtualenv +``` + +4. Create and activate a virtual environment: + +```bash +virtualenv venv +source venv/bin/activate +``` + +5. Install the dependencies: + +```bash +pip install -r requirements.txt +``` + +6. Start the AI controller (you need to run a server first): + +```bash +python3 src/main.py +``` + +## Usage + +To use the AI controller, follow these steps: + +1. Start the AI controller: + +```bash +python3 src/main.py +``` + +2. Use the Pygame interface to play the game as an AI agent. + +3. Monitor the game state and AI interactions in real-time (through the GUI). + +4. You can use the following commands to control the AI agent: +- `Q` to turn left +- `D` to turn right +- `Z` to move forward +- `A` to drop an item +- `E` to take an item +- `T` to broadcast a message +- `F` to fork a new AI agent +- `G` to eject from the tile +- `Space` to start incantation + + +## Contributing + +To contribute to the Zappy API, follow these steps: + +1. Fork the repository. + +2. Create a new branch. + +3. Make your changes. + +4. Commit your changes. + +5. Push your branch. + +6. Create a pull request. diff --git a/bonus/ai-controller/assets/deraumere.png b/bonus/ai-controller/assets/deraumere.png new file mode 100644 index 00000000..cf76bd5a Binary files /dev/null and b/bonus/ai-controller/assets/deraumere.png differ diff --git a/bonus/ai-controller/assets/egg.png b/bonus/ai-controller/assets/egg.png new file mode 100644 index 00000000..a33665dd Binary files /dev/null and b/bonus/ai-controller/assets/egg.png differ diff --git a/bonus/ai-controller/assets/food.png b/bonus/ai-controller/assets/food.png new file mode 100644 index 00000000..09c3983c Binary files /dev/null and b/bonus/ai-controller/assets/food.png differ diff --git a/bonus/ai-controller/assets/linemate.png b/bonus/ai-controller/assets/linemate.png new file mode 100644 index 00000000..d96d47fe Binary files /dev/null and b/bonus/ai-controller/assets/linemate.png differ diff --git a/bonus/ai-controller/assets/mendiane.png b/bonus/ai-controller/assets/mendiane.png new file mode 100644 index 00000000..c9b8a0a5 Binary files /dev/null and b/bonus/ai-controller/assets/mendiane.png differ diff --git a/bonus/ai-controller/assets/phiras.png b/bonus/ai-controller/assets/phiras.png new file mode 100644 index 00000000..bdf9d4d4 Binary files /dev/null and b/bonus/ai-controller/assets/phiras.png differ diff --git a/bonus/ai-controller/assets/player.png b/bonus/ai-controller/assets/player.png new file mode 100644 index 00000000..dd9ab042 Binary files /dev/null and b/bonus/ai-controller/assets/player.png differ diff --git a/bonus/ai-controller/assets/sibur.png b/bonus/ai-controller/assets/sibur.png new file mode 100644 index 00000000..2184366d Binary files /dev/null and b/bonus/ai-controller/assets/sibur.png differ diff --git a/bonus/ai-controller/assets/thystame.png b/bonus/ai-controller/assets/thystame.png new file mode 100644 index 00000000..1bb2ea7a Binary files /dev/null and b/bonus/ai-controller/assets/thystame.png differ diff --git a/bonus/ai-controller/assets/tile.png b/bonus/ai-controller/assets/tile.png new file mode 100644 index 00000000..6bbb5e45 Binary files /dev/null and b/bonus/ai-controller/assets/tile.png differ diff --git a/bonus/ai-controller/requirements.txt b/bonus/ai-controller/requirements.txt new file mode 100644 index 00000000..231dd178 --- /dev/null +++ b/bonus/ai-controller/requirements.txt @@ -0,0 +1 @@ +pygame \ No newline at end of file diff --git a/bonus/ai-controller/src/AI.py b/bonus/ai-controller/src/AI.py new file mode 100644 index 00000000..c391ee87 --- /dev/null +++ b/bonus/ai-controller/src/AI.py @@ -0,0 +1,181 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## AI +## + +import os +import sys +import time +import uuid +import threading + +from Enum.Role import Role +from Network.API import API +from Utils.Message import Message +from Player.Player import Player, Action, Mode +from Network.APIException import APIException + +class AI: + """ + AI class + A class to handle the AI of the Zappy project + + Attributes : + api : API + the API to communicate with the server + player : Player + the player + teamName : str + the name of the team + + ---------- + + Methods : + __init__(host : str, port : int, teamName : str) + Constructor of the AI class + run() + Run the AI + """ + + + def __init__(self, host, port, teamName, logs): + """ + Constructor of the AI class + Assign the API, the player and the team name + + Parameters : + host : str + the host of the server + port : int + the port of the server + teamName : str + the name of the team + """ + self.api = API(host, port, logs) + self.player = Player(teamName, logs) + self.teamName = teamName + self.threads = [] + self.creationTime = time.time_ns() + self.myuuid = str(uuid.uuid4()) + self.isRunning = True + self.buffer = "" + self.logs = logs + + fileName = "" + self.api.connect() + self.api.initConnection(self.teamName, fileName) + + thread = threading.Thread(target=self.serverCommunicationInThread) + thread.start() + self.threads.append(thread) + + + def serverCommunicationInThread(self): + """ + Handle the communication with the server in a thread + """ + while self.isRunning: + responses = self.api.receiveData(0.1) + if responses is not None: + responses = self.buffer + responses + responses = responses.split("\n") + self.buffer = "" + if responses[-1] != "": + self.buffer = responses[-1] + responses.pop() + for response in responses: + if response == '': + continue + self.player.handleResponse(response, self.creationTime, self.teamName, self.myuuid, self.creationTime) + for _ in range(0, len(self.player.callbacks)): + self.player.currentAction = self.player.actions[0] + self.player.currentCommand = self.player.commands[0] + self.player.currentCallback = self.player.callbacks[0] + self.api.sendData(self.player.currentCommand) + while self.player.currentAction != Action.NONE: + responses = self.buffer + self.api.receiveData() + responses = responses.split("\n") + self.buffer = "" + if responses[-1] != "": + self.buffer = responses[-1] + responses.pop() + for response in responses: + if response == '': + continue + self.player.handleResponse(response, self.creationTime, self.teamName, self.myuuid, self.creationTime) + self.player.actions.pop(0) + self.player.commands.pop(0) + self.player.callbacks.pop(0) + self.isRunning = False + + + def actions(self, action): + """ + Send the actions to the server + """ + if len(self.player.actions) > 0: + print("Already doing an action", flush=True, file=sys.stderr) + return + + if action == "Forward": + self.player.moveForward() + elif action == "Right": + self.player.turnRight() + elif action == "Left": + self.player.turnLeft() + + + def takeObject(self, object): + """ + Take an object + """ + self.player.take(object) + + + def setObject(self, object): + """ + Set an object + """ + self.player.set(object) + + + def broadcast(self, message): + """ + Broadcast a message + """ + self.player.broadcast(message) + + + def elevate(self): + """ + Elevate the player + """ + self.player.incantation() + + + def eject(self): + """ + Eject the player + """ + self.player.eject() + + + def fork(self): + """ + Fork the player + """ + self.player.fork() + + + def close(self): + """ + Close the connection + """ + self.api.close() + if self.logs: + sys.stdout.close() + sys.stderr.close() + for thread in self.threads: + thread.join() diff --git a/bonus/ai-controller/src/Enum/Action.py b/bonus/ai-controller/src/Enum/Action.py new file mode 100644 index 00000000..68973cbf --- /dev/null +++ b/bonus/ai-controller/src/Enum/Action.py @@ -0,0 +1,27 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Action +## + +from enum import Enum + +class Action(Enum): + """ + Action class + A class to list the actions the player can do + """ + FORWARD = "Forward" + RIGHT = "Right" + LEFT = "Left" + LOOK = "Look" + INVENTORY = "Inventory" + BROADCAST = "Broadcast" + CONNECT_NBR = "Connect_nbr" + FORK = "Fork" + EJECT = "Eject" + TAKE = "Take" + SET = "Set" + INCANTATION = "Incantation" + NONE = "None" diff --git a/bonus/ai-controller/src/Enum/Item.py b/bonus/ai-controller/src/Enum/Item.py new file mode 100644 index 00000000..c5b263d3 --- /dev/null +++ b/bonus/ai-controller/src/Enum/Item.py @@ -0,0 +1,21 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Item +## + +from enum import Enum + +class Item(Enum): + """ + Item class + A class to list the items in the game + """ + FOOD = "food" + LINEMATE = "linemate" + DERAUMERE = "deraumere" + SIBUR = "sibur" + MENDIANE = "mendiane" + PHIRAS = "phiras" + THYSTAME = "thystame" diff --git a/bonus/ai-controller/src/Enum/Mode.py b/bonus/ai-controller/src/Enum/Mode.py new file mode 100644 index 00000000..4aae8dd0 --- /dev/null +++ b/bonus/ai-controller/src/Enum/Mode.py @@ -0,0 +1,21 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Mode +## + +from enum import Enum + +class Mode(Enum): + FOOD = 0 + STONES = 1 + FORKING = 2 + BROADCASTING = 3 + HANDLINGRESPONSE = 4 + WAITING = 5 + ELEVATING = 6 + REGROUP = 7 + DROPPING = 8 + NONE = 9 + DYING = 10 diff --git a/bonus/ai-controller/src/Enum/Role.py b/bonus/ai-controller/src/Enum/Role.py new file mode 100644 index 00000000..a4cf3169 --- /dev/null +++ b/bonus/ai-controller/src/Enum/Role.py @@ -0,0 +1,13 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Role +## + +from enum import Enum + +class Role(Enum): + UNDEFINED = 0 + LEADER = 1 + SLAVE = 2 diff --git a/bonus/ai-controller/src/Errors/ArgsException.py b/bonus/ai-controller/src/Errors/ArgsException.py new file mode 100644 index 00000000..87f8ec3d --- /dev/null +++ b/bonus/ai-controller/src/Errors/ArgsException.py @@ -0,0 +1,27 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## ArgsException +## + +from Errors.IError import IError + +class ArgsException(IError): + """ + ArgsException class + + A class to handle exceptions that can occur in the Args + The ArgsException class inherits from the IError class + + Attributes : + message : str + the message of the exception + """ + + + def __init__(self, message): + """ + Constructor of the ArgsException class + """ + super().__init__("ArgsException: " + message) diff --git a/bonus/ai-controller/src/Errors/IError.py b/bonus/ai-controller/src/Errors/IError.py new file mode 100644 index 00000000..61a92061 --- /dev/null +++ b/bonus/ai-controller/src/Errors/IError.py @@ -0,0 +1,56 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## IError +## + +from abc import abstractmethod + +@abstractmethod +class IError(Exception): + """ + IError class + + A class to handle errors that can occur in the project + + Attributes : + message : str + the message of the error + + ---------- + + Methods : + __str__() + return the message of the error + __repr__() + return the message of the error + """ + + + def __init__(self, message): + """ + Constructor of the IError class + + Assign the message of the error + + Parameters : + message : str + the message of the error + """ + self.message = message + super().__init__(self.message) + + + def __str__(self): + """ + Return the message of the error + """ + return self.message + + + def __repr__(self): + """ + Return the message of the error + """ + return self.message diff --git a/bonus/ai-controller/src/Network/API.py b/bonus/ai-controller/src/Network/API.py new file mode 100644 index 00000000..14253e2e --- /dev/null +++ b/bonus/ai-controller/src/Network/API.py @@ -0,0 +1,190 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## API +## + +import sys +import socket +import select + +from Utils.Utils import stringifyData +from Network.APIException import APIException + +LIMIT_TRANSFER = 10240 + +class API: + """ + API class + A class to communicate with the server + + Attributes : + host : str + the host of the server + port : int + the port of the server + inputs : list + the list of inputs + outputs : list + the list of outputs + sock : socket + the socket to communicate with the server + + ---------- + + Methods : + sendData(data : str, timeout : int = None) + send data to the server + receiveData(timeout : int = None) + receive data from the server + connect(team_name : str) + connect to the server + close() + close the connection + """ + + + def __init__(self, host : str, port : int, logs : bool): + """ + Constructor of the API class + + Assign the host and the port of the server + Create the socket to communicate with the server + Connect to the server and add the socket to the inputs and outputs lists + + Parameters : + host : str + the host of the server + port : int + the port of the server + """ + self.host : str = host + self.port : int = port + self.inputs : list = [] + self.outputs : list = [] + self.sock : socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.logs = logs + + + def connect(self): + """ + Connect to the server + Add the socket to the inputs and outputs lists + """ + try: + self.sock.connect((self.host, self.port)) + except Exception as e: + raise APIException("connection to the server failed") + self.inputs.append(self.sock) + self.outputs.append(self.sock) + + + def sendData(self, data : str, timeout : int = None): + """ + Send data to the server + + Parameters : + data : str + the data to send + timeout : int + the timeout to wait for the server to be ready to receive data + (default is None which means no timeout) + """ + if -1 in self.outputs: + return + _, write, _ = select.select([], self.outputs, [], timeout) + + if data[-1] != '\n': + data += '\n' + for s in write: + if s == self.sock: + s.send(data.encode()) + if self.logs: + print("sent : ", stringifyData(data), flush=True, file=sys.stderr) + + + def receiveData(self, timeout : float = None): + """ + Receive data from the server + + Parameters : + timeout : float + the timeout to wait for the server to send data + (default is None which means no timeout) + """ + if -1 in self.inputs: + return None + readable, _, _ = select.select(self.inputs, [], [], timeout) + for s in readable: + if s == self.sock: + data = s.recv(LIMIT_TRANSFER) + if not data: + print("Server disconnected") + sys.exit(0) + else: + if self.logs: + print("received :", stringifyData(data.decode()), flush=True, file=sys.stderr) + return data.decode() + return None + + + def initConnection(self, teamName : str, fileName : str = ""): + """ + Function to do the first exchange with the server + + Send the team name to the server + Receive the client number and the map size from the server + Print the client number and the map size + + Parameters : + team_name : str + the name of the team + fileName : str + the file name of logs + + Returns : + client_num : int + the client number + x : int + the x size of the map + y : int + the y size of the map + """ + welcome = self.receiveData() + if welcome != "WELCOME\n": + raise APIException("invalid welcome message", fileName) + + self.sendData(f"{teamName}\n") + received = self.receiveData() + if received == "ko\n": + raise APIException("invalid team name", fileName) + if received.count('\n') == 2: + clientNum, data = received.split('\n', 1) + data = data.split(' ') + else: + clientNum = received.replace('\n', '') + data = self.receiveData() + data = data[0:data.find('\n')].split(' ') + + if len(data) != 2: + raise APIException("invalid map size", fileName) + try: + clientNum = int(clientNum) + x = int(data[0]) + y = int(data[1]) + except Exception as e: + raise APIException("invalid map size", fileName) + + if self.logs: + print("Connected to server") + print(f"Client number: {clientNum}") + print(f"Map size: x = {x}, y = {y}") + return clientNum, x, y + + + def close(self): + """ + Close the connection with the server + """ + self.sock.close() diff --git a/bonus/ai-controller/src/Network/APIException.py b/bonus/ai-controller/src/Network/APIException.py new file mode 100644 index 00000000..4904987b --- /dev/null +++ b/bonus/ai-controller/src/Network/APIException.py @@ -0,0 +1,45 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## APIException +## + +from Errors.IError import IError + +class APIException(IError): + """ + APIException class + + A class to handle exceptions that can occur in the API + The APIException class inherits from the IError class + + Attributes : + message : str + the message of the exception + fileName : str + the file name of logs + + ---------- + + Methods : + __init__(message : str, fileName : str = "") + Constructor of the APIException class + getFileName() + Get the file name of logs + """ + + + def __init__(self, message, fileName=""): + """ + Constructor of the APIException class + """ + self.fileName = fileName + super().__init__("APIException: " + message) + + + def getFileName(self): + """ + Get the file name + """ + return self.fileName diff --git a/bonus/ai-controller/src/Player/Inventory.py b/bonus/ai-controller/src/Player/Inventory.py new file mode 100644 index 00000000..0add6434 --- /dev/null +++ b/bonus/ai-controller/src/Player/Inventory.py @@ -0,0 +1,258 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Inventory +## + +class Inventory: + """ + Inventory class + A class to handle the inventory of the player + + Attributes : + food : int + the number of food + linemate : int + the number of linemate + deraumere : int + the number of deraumere + sibur : int + the number of sibur + mendiane : int + the number of mendiane + phiras : int + the number of phiras + thystame : int + the number of thystame + player : int + the number of players + + ---------- + + Methods : + __init__() + Constructor of the Inventory class + __str__() + Print the inventory + updateInventory(data) + Update the inventory with the data from the inventory command + updateCaseContent(data) + Update the case content with the data from the vision command + addAnObject(ressource) + Add an object to the inventory + removeAnObject(ressource) + Remove an object from the inventory + """ + + + def __init__(self, food=10, linemate=0, deraumere=0, sibur=0, mendiane=0, phiras=0, thystame=0, player=0): + """ + Constructor of the Inventory class + """ + self.food = food + self.linemate = linemate + self.deraumere = deraumere + self.sibur = sibur + self.mendiane = mendiane + self.phiras = phiras + self.thystame = thystame + self.player = player + + + def __str__(self): + """ + Print the inventory + """ + return f"food {self.food}, linemate {self.linemate}, deraumere {self.deraumere}, sibur {self.sibur}, mendiane {self.mendiane}, phiras {self.phiras}, thystame {self.thystame}, player {self.player}" + + + def toStr(self): + """ + Return the inventory as a string + + Returns : + str + the inventory as a string + """ + return f"[food {self.food}, linemate {self.linemate}, deraumere {self.deraumere}, sibur {self.sibur}, mendiane {self.mendiane}, phiras {self.phiras}, thystame {self.thystame}, player {self.player}]" + + + def __eq__(self, inventory): + """ + Compare two inventories + + Parameters : + inventory : Inventory + the inventory to compare with + + Returns : + bool + True if the inventories are the same, False otherwise + """ + if self.food == inventory.food and self.linemate == inventory.linemate and self.deraumere == inventory.deraumere and self.sibur == inventory.sibur and self.mendiane == inventory.mendiane and self.phiras == inventory.phiras and self.thystame == inventory.thystame and self.player == inventory.player: + return True + return False + + + def __add__(self, inventory): + """ + Add two inventories + + Parameters : + inventory : Inventory + the inventory to add + + Returns : + Inventory + the self inventory with the inventory added + """ + return Inventory(self.food + inventory.food, self.linemate + inventory.linemate, self.deraumere + inventory.deraumere, self.sibur + inventory.sibur, self.mendiane + inventory.mendiane, self.phiras + inventory.phiras, self.thystame + inventory.thystame, self.player + inventory.player) + + + def hasMoreStones(self, inventory : "Inventory"): + """ + Check if the self inventory has more stones than the inventory + + Parameters : + inventory : Inventory + the inventory to compare with + + Returns : + bool + True if the self inventory has more stones, False otherwise + """ + if self.linemate > inventory.linemate and self.deraumere > inventory.deraumere and self.sibur > inventory.sibur and self.mendiane > inventory.mendiane and self.phiras > inventory.phiras and self.thystame > inventory.thystame: + return True + return False + + + def updateInventory(self, data : str): + """ + Update the inventory with the data from the inventory command + + Parameters : + data : str + the data from the inventory command + """ + data = data[1:-1] + data = data.split(", ") + for elem in data: + elem = elem.split(" ") + if (elem.count("") > 0): + elem.remove("") + if elem[0] == "food": + self.food = int(elem[1]) + elif elem[0] == "linemate": + self.linemate = int(elem[1]) + elif elem[0] == "deraumere": + self.deraumere = int(elem[1]) + elif elem[0] == "sibur": + self.sibur = int(elem[1]) + elif elem[0] == "mendiane": + self.mendiane = int(elem[1]) + elif elem[0] == "phiras": + self.phiras = int(elem[1]) + elif elem[0] == "thystame": + self.thystame = int(elem[1]) + elif elem[0] == "player": + self.player = int(elem[1]) + + + def updateCaseContent(self, data : list): + """ + Update the case content with the data from the vision command + + Parameters : + data : list + the data from the vision command + """ + for elem in data: + if elem == "food": + self.food += 1 + elif elem == "linemate": + self.linemate += 1 + elif elem == "deraumere": + self.deraumere += 1 + elif elem == "sibur": + self.sibur += 1 + elif elem == "mendiane": + self.mendiane += 1 + elif elem == "phiras": + self.phiras += 1 + elif elem == "thystame": + self.thystame += 1 + elif elem == "player": + self.player += 1 + + + def addAnObject(self, ressource : str): + """ + Add an object to the inventory + + Parameters : + ressource : str + the ressource to add + """ + if ressource == "food": + self.food += 1 + elif ressource == "linemate": + self.linemate += 1 + elif ressource == "deraumere": + self.deraumere += 1 + elif ressource == "sibur": + self.sibur += 1 + elif ressource == "mendiane": + self.mendiane += 1 + elif ressource == "phiras": + self.phiras += 1 + elif ressource == "thystame": + self.thystame += 1 + + + def removeAnObject(self, ressource : str): + """ + Remove an object from the inventory + + Parameters : + ressource : str + the ressource to remove + """ + if ressource == "food" and self.food > 0: + self.food -= 1 + elif ressource == "linemate" and self.linemate > 0: + self.linemate -= 1 + elif ressource == "deraumere" and self.deraumere > 0: + self.deraumere -= 1 + elif ressource == "sibur" and self.sibur > 0: + self.sibur -= 1 + elif ressource == "mendiane" and self.mendiane > 0: + self.mendiane -= 1 + elif ressource == "phiras" and self.phiras > 0: + self.phiras -= 1 + elif ressource == "thystame" and self.thystame > 0: + self.thystame -= 1 + + + def countStones(self): + """ + Count the number of different stones in a case + + Returns : + int + the number of different stones in a case + """ + count = 0 + if self.linemate > 0: + count += 1 + if self.deraumere > 0: + count += 1 + if self.sibur > 0: + count += 1 + if self.mendiane > 0: + count += 1 + if self.phiras > 0: + count += 1 + if self.thystame > 0: + count += 1 + return count diff --git a/bonus/ai-controller/src/Player/Player.py b/bonus/ai-controller/src/Player/Player.py new file mode 100644 index 00000000..016d2274 --- /dev/null +++ b/bonus/ai-controller/src/Player/Player.py @@ -0,0 +1,1110 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Player +## + +import sys +import random + +from Enum.Mode import Mode +from Enum.Role import Role +from Enum.Action import Action +from Utils.Message import Message +from Player.Inventory import Inventory +from Player.PlayerException import PlayerDeathException + +class Player: + """ + Player class + A class to handle the player + + Attributes : + inventory : Inventory + the inventory of the player + level : int + the level of the player + actions : list + the actions of the player + currentAction : Action + the current action of the player + commands : list + the commands of the player + currentCommand : str + the current command of the player + callbacks : list + the callbacks of the player + currentCallback : function + the current callback of the player + vision : list + the vision of the player + broadcastReceived : list + the broadcast received by the player + ejectionReceived : list + the ejection received by the player + isLeader : Role + if the player is the leader/undefined/slave + unusedSlots : int + the unused slots + currentlyElevating : bool + if the player is currently elevating + currentMode : Mode + the current mode of the player + currentFood : int + the current food of the player + nbSlaves : int + the number of slaves that are alive + waitingResponse : bool + if the player is waiting for a response + regroupDirection : int + the direction of the regroup + arrived : bool + if the player arrived to the regroup + isTimed : bool + if the player is timed + nbSlavesHere : int + the number of slaves here + messageHistory : list + the history of the messages + teamName : str + the name of the team + enemyBroadcast : list + the enemy broadcast + ---------- + + Methods : + __init__() + Constructor of the Player class + __str__() + Print the player + moveForward(callback = None) + Move the player forward + turnRight(callback = None) + Turn the player right + turnLeft(callback = None) + Turn the player left + look(callback = None) + Look around the player + cmdInventory(callback = None) + Get the inventory of the player + broadcast(message : str = "Hello", callback = None) + Broadcast a message + connectNbr(callback = None) + Connect to the number of players + fork(callback = None) + Fork the player + eject(callback = None) + Eject the player + take(resource : str = "food", callback = None) + Take a resource + set(resource : str = "food", callback = None) + Set a resource + incantation(callback = None) + Start the incantation + none() + Do nothing + updateVision(vision : str) + Update the vision of the player + updateInventory(inventory : str) + Update the inventory of the player + updateBroadcastReceived(message : str) + Update the broadcast received by the player + updateEjectionReceived(message : str) + Update the ejection received by the player + updateLevel(level : int) + Update the level of the player + handleElevation(response : str) + Handle the elevation + hasSomethingHappened(response : str) + Check if something happened + handleResponse(response : str) + Handle the response + connectMissingPlayers() + Connect the missing players + completeTeam() + Complete the team + updateModeSlave() + Update the mode of the player when he is a slave + updateModeLeader() + Update the mode of the player when he is a leader + updateMode() + Update the mode of the player + lookingForFood() + Look for food + lookingForStones() + Look for stones + askSlavesForInventory() + Ask the slaves for their inventory + checkIfEnoughFood(response : str) + Check if the slave has enough food + handleResponseBroadcast() + Handle the response of the broadcast + slavesReponses() + Handle the leader order as a slave + waitingEveryone() + Wait for everyone to finish the regroup + waitingDrop() + Wait for everyone to finish droping the stones + dropping() + Drop the stones + regroupAction() + Regroup the players + chooseAction() + Choose the action of the player + """ + + + def __init__(self, teamName : str, logs : bool = False): + """ + Constructor of the Player class + """ + self.inventory = Inventory() + self.level = 1 + self.actions = [] + self.currentAction = Action.NONE + self.commands = [] + self.currentCommand = "" + self.callbacks = [] + self.currentCallback = None + self.vision = [] + self.broadcastReceived = [] + self.ejectionReceived = [] + self.isLeader = Role.UNDEFINED + self.unusedSlots = 0 + self.currentlyElevating = False + self.currentMode = Mode.FOOD + self.currentFood = 0 + self.nbSlaves = 0 + self.waitingResponse = False + self.regroupDirection = 0 + self.arrived = False + self.isTimed = False + self.nbSlavesHere = 0 + self.messageHistory = [] + self.teamName = teamName + self.enemyBroadcast = [] + self.alliesUuid = [] + self.logs = logs + + + def __str__(self): + """ + Print the player + """ + return f"Level: {self.level}, Inventory: [{self.inventory}], Current action: {self.currentAction}, Current command: {self.currentCommand}, Vision: {self.vision}, Broadcast received: {self.broadcastReceived}, Ejection received: {self.ejectionReceived}" + + + def moveForward(self, callback = None): + """ + Set the current action to forward + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.FORWARD) + self.commands.append("Forward") + self.callbacks.append(callback) + + + def turnRight(self, callback = None): + """ + Set the current action to right + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.RIGHT) + self.commands.append("Right") + self.callbacks.append(callback) + + + def turnLeft(self, callback = None): + """ + Set the current action tl moderation bot designed for mo left + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.LEFT) + self.commands.append("Left") + self.callbacks.append(callback) + + + def look(self, callback = None): + """ + Set the current action to look + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.LOOK) + self.commands.append("Look") + self.callbacks.append(callback) + + + def cmdInventory(self, callback = None): + """ + Set the current action to inventory + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.INVENTORY) + self.commands.append("Inventory") + self.callbacks.append(callback) + + + def broadcast(self, message : str = "Hello"): + """ + Set the current action to broadcast + + Parameters : + message : str + the message to broadcast + callback : function + the callback to call after the action + (default is None) + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + print(f"Broadcasting: {message}", flush=True, file=sys.stderr) + self.actions.append(Action.BROADCAST) + self.commands.append(f"Broadcast \"{message}\"") + self.callbacks.append(None) + self.messageHistory.append(message) + + + def broadcastEnemyMessage(self, callback = None): + """ + Set the current action to broadcast an enemy message + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + message = (1, "English or Spanish?") + if len(self.enemyBroadcast) > 0: + message = random.choice(self.enemyBroadcast) + self.actions.append(Action.BROADCAST) + self.commands.append(f"Broadcast \"{message[1]}\"") + self.callbacks.append(callback) + + + def connectNbr(self, callback = None): + """ + Set the current action to connect_nbr + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.CONNECT_NBR) + self.commands.append("Connect_nbr") + self.callbacks.append(callback) + + + def fork(self, callback = None): + """ + Set the current action to fork + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.FORK) + self.commands.append("Fork") + self.callbacks.append(callback) + + + def eject(self, callback = None): + """ + Set the current action to eject + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.EJECT) + self.commands.append("Eject") + self.callbacks.append(callback) + + + def take(self, resource : str = "food", callback = None): + """ + Set the current action to take + + Parameters : + resource : str + the resource to take + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.TAKE) + self.commands.append(f"Take {resource}") + self.callbacks.append(callback) + + + def set(self, resource : str = "food", callback = None): + """ + Set the current action to set + + Parameters : + resource : str + the resource to set + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.SET) + self.commands.append(f"Set {resource}") + self.callbacks.append(self.inventory.removeAnObject(resource)) + + + def incantation(self, callback = None): + """ + Set the current action to incantation + + Parameters : + callback : function + the callback to call after the action + (default is None) + """ + self.actions.append(Action.INCANTATION) + self.commands.append("Incantation") + self.callbacks.append(callback) + + + def none(self): + """ + Set the current action to none + """ + self.actions.append(Action.NONE) + self.commands.append("") + self.callbacks.append(None) + + + def updateVision(self, vision : str): + """ + Update the vision of the player with the data from the look command + + Parameters : + vision : str + the vision from the server + """ + vision = vision[1:-1] + vision = vision.split(',') + self.vision = [] + for case in vision: + inventory = Inventory(0, 0, 0, 0, 0, 0, 0, 0) + inventory.updateCaseContent(case.split(" ")) + self.vision.append(inventory) + if self.currentCallback is not None: + self.currentCallback() + return + + + def updateInventory(self, inventory : str): + """ + Update the inventory of the player with the data from the inventory command + + Parameters : + inventory : str + the inventory from the server + """ + self.inventory.updateInventory(inventory) + + + def updateBroadcastReceived(self, message : str, aiTimestamp : int): + """ + Update the broadcast received by the player + + Parameters : + message : str + the message from the server + aiTimestamp : int + the timestamp of the AI + """ + message = message[8:] + direction = int(message.split(", ")[0]) + if message.find('\"') != -1: + message = message[message.find('\"') + 1: message.rfind('\"')] + else: + message = message.split(", ")[1] + msg = Message(self.teamName) + if msg.createMessageFromEncryptedJson(message): + if self.logs: + print("Received message: ", msg.message, flush=True, file=sys.stderr) + if msg in self.messageHistory or msg.messageTimestamp < aiTimestamp: + if self.logs: + print("Already received this message", flush=True, file=sys.stderr) + return + self.broadcastReceived.append((direction, msg)) + self.messageHistory.append(msg) + if self.isLeader == Role.LEADER: + if msg.senderUuid not in self.alliesUuid: + self.alliesUuid.append(msg.senderUuid) + else: + if self.logs: + print("Received enemy message: ", message, flush=True, file=sys.stderr) + self.enemyBroadcast.append((direction, message)) + + + def updateEjectionReceived(self, message : str): + """ + Update the ejection received by the player + + Parameters : + message : str + the message from the server + """ + message = message[7:] + direction = int(message) + self.ejectionReceived.append(direction) + + + def updateLevel(self, level : int): + """ + Update the level of the player + + Parameters : + level : int + the level of the player + """ + self.level = level + + + def handleElevation(self, response : str, teamName : str, myuuid : str, creationTime : int): + """ + Handle the response of the elevation command + + Parameters : + response : str + the response from the server + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + if response == "Elevation underway": + self.currentlyElevating = True + if self.isLeader == Role.SLAVE: + self.currentMode = Mode.NONE + return True + elif response.startswith("Current level:"): + self.updateLevel(int(response.split(" ")[2])) + self.currentlyElevating = False + return False + elif response == "ko": + if self.logs: + print("Elevation failed", flush=True, file=sys.stderr) + if self.isLeader == Role.LEADER: + self.currentMode = Mode.FOOD + self.broadcast("Food") + self.currentlyElevating = False + return False + + + def hasSomethingHappened(self, response : str, aiTimestamp : int): + """ + Check if something happened to the player + Look if the player is dead, if he received a message or if he was ejected + + Parameters : + response : str + the response from the server + aiTimestamp : int + the timestamp of the AI + """ + if response == "dead": + raise PlayerDeathException("Player is dead") + elif response.startswith("message"): + self.updateBroadcastReceived(response, aiTimestamp) + return True + elif response.startswith("eject:"): + self.updateEjectionReceived(response) + return True + return False + + + def handleResponse(self, response : str, aiTimestamp : int, teamName : str, myuuid : str, creationTime : int): + """ + Handle the response from the server + + Parameters : + response : str + the response from the server + aiTimestamp : int + the timestamp of the AI + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + if self.hasSomethingHappened(response, aiTimestamp) or self.currentMode == Mode.DYING: + return + if response == "ko" and self.currentAction != Action.INCANTATION: + self.currentAction = Action.NONE + self.currentCommand = "" + return + if response == "ok": + if self.currentCallback is not None: + self.currentCallback() + elif self.currentAction == Action.LOOK: + self.updateVision(response) + elif self.currentAction == Action.INVENTORY: + self.updateInventory(response) + elif self.currentAction == Action.CONNECT_NBR: + self.unusedSlots = int(response) + if self.currentCallback is not None: + self.currentCallback() + elif self.currentAction == Action.INCANTATION: + if self.handleElevation(response, teamName, myuuid, creationTime): + return + self.currentAction = Action.NONE + self.currentCommand = "" + self.callback = None + if self.currentMode == Mode.REGROUP and self.isLeader == Role.SLAVE: + if response == "ok": + self.broadcastReceived = [] + + + def connectMissingPlayers(self): + """ + Connect the missing players + """ + if self.logs: + print("Connecting missing players", flush=True, file=sys.stderr) + for _ in range(0, min(self.unusedSlots, 5)): + from AI import forkAI + forkAI() + + + def completeTeam(self): + """ + Complete the team + """ + self.connectNbr(self.connectMissingPlayers) + + + def updateModeSlave(self): + """ + Update the mode of the player when he is a slave + """ + if self.inventory.food < 35: + self.currentMode = Mode.FOOD + elif self.inventory.food >= 45: + self.currentMode = Mode.STONES + + + def updateModeLeader(self): + """ + Update the mode of the player when he is a leader + """ + if self.inventory.food < 35: + self.currentMode = Mode.FOOD + elif self.inventory.food >= 45 or self.currentMode != Mode.FOOD: + if self.logs: + print(self.currentFood, self.inventory.food) + if self.currentFood != self.inventory.food and self.waitingResponse == True: + if self.isTimed == True: + if self.logs: + print("Handling response") + self.currentMode = Mode.HANDLINGRESPONSE + self.isTimed = False + else: + self.isTimed = True + elif self.currentFood != self.inventory.food and self.waitingResponse == False: + if self.logs: + print("Broadcasting") + self.currentMode = Mode.BROADCASTING + self.waitingResponse = True + elif self.nbSlaves < 5 and self.waitingResponse == False: + self.currentMode = Mode.FORKING + else: + self.currentMode = Mode.WAITING + self.currentFood = self.inventory.food + + + def updateMode(self): + """ + Update the mode of the player + """ + if self.currentMode == Mode.REGROUP or self.currentMode == Mode.DROPPING or self.currentMode == Mode.ELEVATING or self.currentMode == Mode.NONE: + return + if self.isLeader == Role.LEADER: + self.updateModeLeader() + else: + self.updateModeSlave() + + + def lookingForFood(self): + """ + Look for food + The player will look for the nearest food in his vision. + When he finds food, he will go to the case + where there is food and take it. + """ + index = -1 + order = [0, 2, 1, 3] + for i in order: + if len(self.vision) > i and self.vision[i].food > 0: + index = i + break + if index == -1: + randAction = random.choice([self.moveForward, self.turnLeft, self.turnRight]) + randAction() + self.moveForward() + self.cmdInventory() + return + if index == 1: + self.moveForward() + self.turnLeft() + self.moveForward() + elif index == 2: + self.moveForward() + elif index == 3: + self.moveForward() + self.turnRight() + self.moveForward() + for i in range(0, self.vision[index].food): + if len(self.actions) < 9: + self.take("food") + else: + break + self.cmdInventory() + + + def lookingForStones(self): + """ + Look for stones + The player will look for the case with the most stones in his vision. + When he finds stones, he will go to the case + where there are stones and take them. + """ + index = -1 + count = 0 + order = [0, 2, 1, 3] + for i in order: + if len(self.vision) > i and self.vision[i].countStones() > count: + index = i + count = self.vision[i].countStones() + if index == -1: + self.moveForward() + self.moveForward() + self.cmdInventory() + return + if index == 1: + self.moveForward() + self.turnLeft() + self.moveForward() + elif index == 2: + self.moveForward() + elif index == 3: + self.moveForward() + self.turnRight() + self.moveForward() + if self.vision[index].linemate > 0: + self.take("linemate") + if self.vision[index].deraumere > 0: + self.take("deraumere") + if self.vision[index].sibur > 0: + self.take("sibur") + if self.vision[index].mendiane > 0: + self.take("mendiane") + if self.vision[index].phiras > 0: + self.take("phiras") + if self.vision[index].thystame > 0: + self.take("thystame") + self.cmdInventory() + + + def askSlavesForInventory(self, teamName : str, myuuid : str, creationTime : int): + """ + Ask the slaves for their inventory + The leader will ask the slaves for their inventory + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + self.broadcast("Inventory") + self.nbSlaves = 0 + + + def checkIfEnoughFood(self, response : str): + """ + Check if the slave has enough food to survive the regroup + + Parameters : + response : str + the response from the slave + """ + inv = Inventory(0, 0, 0, 0, 0, 0, 0, 0) + inv.updateInventory(response) + if inv.food < 35: + return False + return True + + + def isMessageInventory(self, message : str): + """ + Check if the message is an inventory message + + Parameters : + message : str + the message + """ + if message.startswith("[") and message.endswith("]") and message.count(",") == 7: + return True + return False + + + def countSlavesThatHaveSendInventory(self, messages : list): + """ + Count the number of slaves that have send their inventory + + Parameters : + messages : list + the messages received by the player + """ + nbSlaves = 0 + sendersUuid = [] + for msg in messages: + if self.isMessageInventory(msg[1].message) and msg[1].senderUuid not in sendersUuid: + nbSlaves += 1 + sendersUuid.append(msg[1].senderUuid) + return nbSlaves + + + def handleResponseBroadcast(self): + """ + Handle the response of the broadcast + """ + if self.logs: + print(self.broadcastReceived, flush=True) + self.nbSlaves = len(self.broadcastReceived) + if self.logs: + print("nb slaves: ", self.nbSlaves, flush=True) + globalInv = Inventory(0, 0, 0, 0, 0, 0, 0, 0) + minInv = Inventory(0, 8, 9, 10, 5, 6, 1, 0) + if self.nbSlaves >= 5: + for response in self.broadcastReceived: + if self.checkIfEnoughFood(response[1].message) == False: + self.waitingResponse = False + self.broadcastReceived = [] + return + inv = Inventory(0, 0, 0, 0, 0, 0, 0, 0) + inv.updateInventory(response[1].message) + globalInv = globalInv + inv + if globalInv.hasMoreStones(minInv): + self.currentMode = Mode.REGROUP + else: + if self.logs: + print("Not enough stones", flush=True, file=sys.stdout) + print("Not enough stones", flush=True, file=sys.stderr) + self.waitingResponse = False + self.broadcastReceived = [] + + + def slavesReponses(self, teamName : str, myuuid : str, creationTime : int): + """ + Handle the leader order as a slave + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + for broadcast in self.broadcastReceived: + if broadcast[1].message == "Inventory": + response = self.inventory.toStr() + self.broadcast(response) + if broadcast[1].message == "Regroup": + self.currentMode = Mode.REGROUP + self.regroupDirection = broadcast[0] + return + + + def countSlavesThatArrived(self, messages : list): + """ + Count the number of slaves that arrived to the regroup + + Parameters : + messages : list + the messages received by the player + """ + nbSlavesHere = 0 + sendersUuid = [] + for msg in messages: + if msg[1].message == "I'm here" and msg[1].senderUuid not in sendersUuid: + nbSlavesHere += 1 + sendersUuid.append(msg[1].senderUuid) + return nbSlavesHere + + + def waitingEveryone(self, teamName : str, myuuid : str, creationTime : int): + """ + Wait for everyone to finish the regroup + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + nbSlavesHere = self.countSlavesThatArrived(self.broadcastReceived) + if self.logs: + print("nb slaves here: ", nbSlavesHere, flush=True) + if nbSlavesHere >= 5: + self.broadcast("Drop", teamName, myuuid, creationTime) + self.currentMode = Mode.DROPPING + self.broadcastReceived = [] + else: + self.broadcast("Regroup", teamName, myuuid, creationTime) + + + def countSlavesThatFinishedDroping(self, messages : list): + """ + Count the number of slaves that finished droping the stones + + Parameters : + messages : list + the messages received by the player + """ + nbSlavesHere = 0 + sendersUuid = [] + for msg in messages: + if msg[1].message == "Finished dropping" and msg[1].senderUuid not in sendersUuid: + nbSlavesHere += 1 + sendersUuid.append(msg[1].senderUuid) + return nbSlavesHere + + + def waitingDrop(self): + """ + Wait for everyone to finish droping the stones + """ + nbSlavesHere = self.countSlavesThatFinishedDroping(self.broadcastReceived) + minStoneCase = Inventory(0, 8, 9, 10, 5, 6, 1, 0) + currentCase = self.vision[0] + if currentCase.hasMoreStones(minStoneCase): + self.currentMode = Mode.ELEVATING + self.broadcastReceived = [] + self.nbSlavesHere = nbSlavesHere + if self.logs: + print("nb slaves who finished droping: ", nbSlavesHere, flush=True) + if nbSlavesHere >= 5: + self.currentMode = Mode.ELEVATING + self.broadcastReceived = [] + else: + self.look() + + + def dropping(self, teamName : str, myuuid : str, creationTime : int): + """ + Drop the stones + As a leader, you will wait for the slaves to drop the stones + As a slave, you will drop the stones until you have none left + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + if self.isLeader == Role.LEADER: + self.waitingDrop() + else: + if self.logs: + print("Dropping", flush=True, file=sys.stderr) + if self.inventory.linemate > 0: + self.set("linemate") + if self.inventory.deraumere > 0: + self.set("deraumere") + if self.inventory.sibur > 0: + self.set("sibur") + if self.inventory.mendiane > 0: + self.set("mendiane") + if self.inventory.phiras > 0: + self.set("phiras") + if self.inventory.thystame > 0: + self.set("thystame") + if self.inventory.linemate == 0 and self.inventory.deraumere == 0 and self.inventory.sibur == 0 and self.inventory.mendiane == 0 and self.inventory.phiras == 0 and self.inventory.thystame == 0: + self.broadcast("Finished dropping", teamName, myuuid, creationTime) + self.currentMode = Mode.NONE + return + + + def regroupAction(self, teamName : str, myuuid : str, creationTime : int): + """ + Regroup the players + As a leader, you will wait for the slaves to regroup + As a slave, you will regroup with the leader + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + if self.isLeader == Role.LEADER: + self.waitingEveryone(teamName, myuuid, creationTime) + else: + isThereARegroup = False + + if len(self.broadcastReceived) != 0 and self.logs: + print(self.broadcastReceived, flush=True, file=sys.stderr) + for broadcast in self.broadcastReceived: + if broadcast[1].message == "Drop": + if self.logs: + print("DROP MODE", flush=True, file=sys.stderr) + self.currentMode = Mode.DROPPING + self.broadcastReceived = [] + return + if broadcast[1].message == "Regroup": + isThereARegroup = True + self.regroupDirection = broadcast[0] + + self.broadcastReceived = [] + if isThereARegroup == False: + return + if self.regroupDirection == 0 and self.arrived == False: + self.broadcast("I'm here", teamName, myuuid, creationTime) + self.arrived = True + if self.regroupDirection == 3: + self.turnLeft() + if self.regroupDirection == 7: + self.turnRight() + if self.regroupDirection == 1: + self.moveForward() + if self.regroupDirection == 5: + self.turnRight() + self.turnRight() + if self.regroupDirection == 2 or self.regroupDirection == 8: + self.moveForward() + if self.regroupDirection == 4 or self.regroupDirection == 6: + self.turnRight() + self.turnRight() + + + def chooseAction(self, teamName : str, myuuid : str, creationTime : int): + """ + Choose the action of the player + The action is chosen depending on the mode of the player + The mode is updated before choosing the action + + Parameters : + teamName : str + the name of the team + myuuid : str + the uuid of the player + creationTime : int + the creation time of the message + """ + if self.currentMode == Mode.DYING: + return + if self.inventory.food <= 1 and self.isLeader == Role.LEADER: + self.broadcast(random.choice(self.alliesUuid), teamName, myuuid, creationTime) + self.currentMode = Mode.DYING + return + if self.isLeader == Role.LEADER: + for msg in self.broadcastReceived: + if msg[1].message == "IsLeader?": + self.broadcast("Yes", teamName, myuuid, creationTime) + self.broadcastReceived.remove(msg) + if self.isLeader == Role.SLAVE: + for msg in self.broadcastReceived: + if msg[1].message == "Food": + self.currentMode = Mode.FOOD + self.arrived = False + self.broadcastReceived.remove(msg) + if msg[1].message == myuuid: + self.isLeader = Role.LEADER + self.currentMode = Mode.FOOD + self.arrived = False + self.broadcastReceived.remove(msg) + self.updateMode() + if self.currentMode == Mode.REGROUP: + self.regroupAction(teamName, myuuid, creationTime) + return + if self.currentMode == Mode.DROPPING: + self.dropping(teamName, myuuid, creationTime) + return + if self.isLeader == Role.SLAVE: + if len(self.broadcastReceived) > 0: + self.slavesReponses(teamName, myuuid, creationTime) + self.broadcastReceived = [] + if self.currentMode == Mode.FOOD: + self.look(self.lookingForFood) + elif self.currentMode == Mode.STONES: + self.look(self.lookingForStones) + return + elif self.currentMode == Mode.FORKING: + if self.logs: + print("Forking") + from AI import forkAI + self.fork(forkAI) + self.nbSlaves += 1 + self.cmdInventory() + return + elif self.currentMode == Mode.BROADCASTING: + if self.logs: + print("in broadcast") + self.askSlavesForInventory(teamName, myuuid, creationTime) + self.cmdInventory() + return + elif self.currentMode == Mode.HANDLINGRESPONSE: + self.handleResponseBroadcast() + self.cmdInventory() + return + elif self.currentMode == Mode.WAITING: + self.cmdInventory() + rand = random.randint(0, 5) + if rand == 0: + self.broadcastEnemyMessage() + else: + self.look() + return + elif self.currentMode == Mode.ELEVATING: + self.incantation() + return + elif self.currentMode == Mode.NONE: + return + return diff --git a/bonus/ai-controller/src/Player/PlayerException.py b/bonus/ai-controller/src/Player/PlayerException.py new file mode 100644 index 00000000..0784e4f7 --- /dev/null +++ b/bonus/ai-controller/src/Player/PlayerException.py @@ -0,0 +1,47 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## PlayerException +## + +from Errors.IError import IError + +class PlayerException(IError): + """ + PlayerException class + + A class to handle exceptions that can occur in the Player + The PlayerException class inherits from the IError class + + Attributes : + message : str + the message of the exception + """ + + + def __init__(self, message): + """ + Constructor of the PlayerException class + """ + super().__init__("PlayerException: " + message) + + +class PlayerDeathException(PlayerException): + """ + PlayerDeathException class + + A class to handle the death of the player + The PlayerDeathException class inherits from the PlayerException class + + Attributes : + message : str + the message of the exception + """ + + + def __init__(self, message): + """ + Constructor of the PlayerDeathException class + """ + super().__init__("PlayerDeathException: " + message) diff --git a/bonus/ai-controller/src/Utils/Message.py b/bonus/ai-controller/src/Utils/Message.py new file mode 100644 index 00000000..1e0fff35 --- /dev/null +++ b/bonus/ai-controller/src/Utils/Message.py @@ -0,0 +1,184 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Message +## + +import uuid +import json +import time + +class Message: + """ + Message class + A class to handle messages when AI is broadcasting + This class is used to create, encrypt and decrypt messages + + Attributes : + messageUuid : str + the uuid of the message + messageTimestamp : int + the timestamp of the message + message : str + the message + senderUuid : str + the uuid of the sender + senderCreationTime : int + the creation time of the sender + key : int + the key to encrypt and decrypt the message + + ---------- + + Methods : + __init__(key : str) + Constructor of the Message class + createMessage(message : str, senderUuid : str, senderCreationTime : int) + Create a message + createMessageFromJson(jsonStr : str) + Create a message from a json string + createMessageFromEncryptedJson(jsonStr : str) + Create a message from an encrypted json string + __str__() + Return the message as a json string + __repr__() + Return the message as a json string + __eq__(other) + Compare two messages + __ne__(other) + Compare two messages + encrypt() + Encrypt the message + decrypt(cipher) + Decrypt the message + """ + + + def __init__(self, key : str): + """ + Constructor of the Message class + + Parameters : + key : str + the key to encrypt and decrypt the message + (the key is the team name of the AI) + """ + self.messageUuid = str(uuid.uuid4()) + self.messageTimestamp = 0 + self.message = "" + self.senderUuid = "" + self.senderCreationTime = 0 + self.key = 0 + for char in key: + self.key += ord(char) + + + def createMessage(self, message : str, senderUuid : str, senderCreationTime : int): + """ + Create a message and assign the message, the sender uuid and the sender creation time + + Parameters : + message : str + the message + senderUuid : str + the uuid of the sender + senderCreationTime : int + the creation time of the sender + """ + self.message = message + self.senderUuid = senderUuid + self.senderCreationTime = senderCreationTime + self.messageTimestamp = int(time.time_ns()) + + + def createMessageFromJson(self, jsonStr : str): + """ + Create a message from a json string + And assign the message, the sender uuid, the sender creation time, the message uuid and the message timestamp + + Parameters : + jsonStr : str + the json string + """ + try: + jsonStr = json.loads(jsonStr) + self.message = jsonStr["message"] + self.senderUuid = jsonStr["senderUuid"] + self.senderCreationTime = jsonStr["senderCreationTime"] + self.messageUuid = jsonStr["messageUuid"] + self.messageTimestamp = jsonStr["messageTimestamp"] + except: + return False + return True + + + def createMessageFromEncryptedJson(self, jsonStr : str): + """ + Create a message from an encrypted json string, decrypt it + And assign the message, the sender uuid, the sender creation time, the message uuid and the message timestamp + + Parameters : + jsonStr : str + the json string + """ + jsonStr = self.decrypt(jsonStr) + return self.createMessageFromJson(jsonStr) + + + def __str__(self): + """ + Return the message as a json string + """ + jsonStr = {"message": self.message, "senderUuid": self.senderUuid, "senderCreationTime": self.senderCreationTime, "messageUuid": self.messageUuid, "messageTimestamp": self.messageTimestamp} + return json.dumps(jsonStr) + + + def __repr__(self): + """ + Return the message as a json string + """ + return self.__str__() + + + def __eq__(self, other): + """ + Compare two messages + Check if the message, the sender uuid, the sender creation time, the message uuid and the message timestamp are the same + """ + return self.message == other.message and self.senderUuid == other.senderUuid and self.senderCreationTime == other.senderCreationTime and self.messageUuid == other.messageUuid and self.messageTimestamp == other.messageTimestamp + + + def __ne__(self, other): + """ + Compare two messages + Check if the message, the sender uuid, the sender creation time, the message uuid and the message timestamp are different + """ + return not self.__eq__(other) + + + def encrypt(self): + """ + Encrypt the message using the key + The message is encrypted using the XOR operator + """ + encryptedMessage = "" + initialMessage = self.__str__() + for i in range(len(initialMessage)): + encryptedMessage += chr(ord(initialMessage[i]) ^ self.key) + return encryptedMessage + + + def decrypt(self, cipher): + """ + Decrypt the message using the key + The message is decrypted using the XOR operator + + Parameters : + cipher : str + the encrypted message + """ + decryptedMessage = "" + for i in range(len(cipher)): + decryptedMessage += chr(ord(cipher[i]) ^ self.key) + return decryptedMessage diff --git a/bonus/ai-controller/src/Utils/Utils.py b/bonus/ai-controller/src/Utils/Utils.py new file mode 100644 index 00000000..fece80f7 --- /dev/null +++ b/bonus/ai-controller/src/Utils/Utils.py @@ -0,0 +1,90 @@ +## +## EPITECH PROJECT, 2024 +## Zappy +## File description: +## Utils +## + +def stringifyData(data : str): + """ + Transform data to a string to print it + + Parameters : + data : str + the data to transform + """ + string = "\"" + for c in data: + if c == '\n': + string += "\\n" + elif c == '\t': + string += "\\t" + else: + string += c + string += "\"" + return string + + +def mapRangeOpti(n): + """ + Function to get map's index view but optimized + Parameters : + n : int + Vision length + Returns : + List of indexes + """ + yield 1 + yield 0 + for i in range(2, n): + yield i + + +def getXmovement(middle, max, width, target): + """ + Get the horizontal movements to do to reach the target tile + + Parameters : + middle : int + index of the middle tile + max : int + index of the last tile on the row + width : int + width of the current row + + Returns : + int : the number of movements to do + """ + if middle == target: + return 0 + return target - middle + + +def getMovesTowardTile(index): + """ + Return the XY movements to do to reach the tile at index X + + Parameters : + index : int + Index of the tile to reach + + Returns : + tuple : (int, int) movements to do + """ + maxRowNum = 3 + crowWidth = 3 + fwdRow = 1 + middleTileIndex = 2 + + if index == 0: + return (0, 0) + if index <= maxRowNum: + return (getXmovement(middleTileIndex, maxRowNum, crowWidth, index), 1) + for i in range(7): + fwdRow += 1 + crowWidth += 2 + middleTileIndex += fwdRow*2 + maxRowNum += crowWidth + if index <= maxRowNum: + return (getXmovement(middleTileIndex, maxRowNum, crowWidth, index), fwdRow) + return -1 diff --git a/bonus/ai-controller/src/main.py b/bonus/ai-controller/src/main.py new file mode 100644 index 00000000..63d37a61 --- /dev/null +++ b/bonus/ai-controller/src/main.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +## +## EPITECH PROJECT, 2024 +## Zappy AI +## File description: +## main +## + +import sys +import pygame + +from AI import AI +from pygame.locals import * +from Errors.ArgsException import ArgsException +from Player.PlayerException import PlayerDeathException + +# Port min +PORT_MIN = 0 +# Port max +PORT_MAX = 65535 +# Localhost +LOCALHOST = "127.0.0.1" + +pygame.init() +DISPLAYSURF = pygame.display.set_mode((500, 100)) + +def writeHelp(exitCode : int = 0): + """ + Print the help message + """ + print("") + print("Usage:") + print("\t./zappy_ai -p port -n name -h machine [-l on/off]") + print("") + print("Description:") + print("\t-p port\t\tis the port number") + print("\t-n name\t\tis the name of the team") + print("\t-h machine\tis the name of the machine; localhost by default") + print("\t-l on/off\tturn logs on or off; off by default") + print("\t--help\t\tprint this help") + print("") + sys.exit(exitCode) + + +def getArgs(av=sys.argv): + """ + Get the arguments + + Parameters: + av : list + the arguments passed to the program + """ + if len(av) == 2 and av[1] == "--help": + writeHelp(0) + host = LOCALHOST + port = -1 + name = "" + logs = False + try: + for i in range(1, len(av)): + if av[i] == "-p": + port = int(av[i + 1]) + elif av[i] == "-n": + name = (av[i + 1]).replace("\n", "") + elif av[i] == "-h": + host = av[i + 1] + elif av[i] == "-l": + if av[i + 1].lower() == "on": + logs = True + elif av[i + 1].lower() == "off": + logs = False + else: + raise ArgsException("Error: invalid arguments") + if port < PORT_MIN or port > PORT_MAX or name == "": + raise ArgsException("Error: invalid arguments") + except Exception as e: + print("Error: invalid arguments", file=sys.stderr, flush=True) + writeHelp(84) + return host, port, name, logs + + +class Item(pygame.sprite.Sprite): + def __init__(self, x, y, path): + super().__init__() + self.image = pygame.image.load(path) + self.image = pygame.transform.scale(self.image, (50, 50)) + self.rect = self.image.get_rect() + self.rect.center = (x, y) + + def draw(self, surface): + surface.blit(self.image, self.rect) + +class Text(pygame.sprite.Sprite): + def __init__(self, x, y, text): + super().__init__() + self.font = pygame.font.Font(None, 36) + self.image = self.font.render(text, 1, (255, 255, 255)) + self.rect = self.image.get_rect() + self.rect.center = (x, y) + self.x = x + self.y = y + + def updateText(self, text): + self.image = self.font.render(text, 1, (255, 255, 255)) + self.rect = self.image.get_rect() + self.rect.center = (self.x, self.y) + + def draw(self, surface): + surface.blit(self.image, self.rect) + +startingX = 40 +food = Item(25 + startingX, 70, "assets/food.png") +linemate = Item(90 + startingX, 70, "assets/linemate.png") +deraumere = Item(150 + startingX, 70, "assets/deraumere.png") +sibur = Item(210 + startingX, 70, "assets/sibur.png") +mendiane = Item(270 + startingX, 70, "assets/mendiane.png") +phiras = Item(330 + startingX, 70, "assets/phiras.png") +thystame = Item(390 + startingX, 70, "assets/thystame.png") +actionText = Text(250, 20, "Actions: ") + +def handlingEvents(ai : AI): + """ + Handle the events + """ + for event in pygame.event.get(): + if event.type == QUIT: + return False + if event.type == KEYDOWN: + if event.key == K_ESCAPE: + return False + if event.key == K_z: + ai.actions("Forward") + if event.key == K_d: + ai.actions("Right") + if event.key == K_q: + ai.actions("Left") + if event.key == K_g: + ai.eject() + if event.key == K_SPACE: + ai.elevate() + if event.key == K_f: + ai.fork() + if event.key == K_t: + print("Enter the message to broadcast: ", end="") + message = input() + ai.broadcast(message) + + if event.key == K_a or event.key == K_e: + print("Click on the object you want to take/drop") + if event.key == K_a: + action = ai.setObject + if event.key == K_e: + action = ai.takeObject + elem = "" + while ai.isRunning: + if ai.threads[0].is_alive() == False: + ai.isRunning = False + print("Player died", file=sys.stderr, flush=True) + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + return False + if event.type == MOUSEBUTTONDOWN: + if food.rect.collidepoint(event.pos): + elem = "food" + if linemate.rect.collidepoint(event.pos): + elem = "linemate" + if deraumere.rect.collidepoint(event.pos): + elem = "deraumere" + if sibur.rect.collidepoint(event.pos): + elem = "sibur" + if mendiane.rect.collidepoint(event.pos): + elem = "mendiane" + if phiras.rect.collidepoint(event.pos): + elem = "phiras" + if thystame.rect.collidepoint(event.pos): + elem = "thystame" + if elem != "": + break + if elem != "": + break + DISPLAYSURF.fill((0, 0, 0)) + food.draw(DISPLAYSURF) + linemate.draw(DISPLAYSURF) + deraumere.draw(DISPLAYSURF) + sibur.draw(DISPLAYSURF) + mendiane.draw(DISPLAYSURF) + phiras.draw(DISPLAYSURF) + thystame.draw(DISPLAYSURF) + actionText.draw(DISPLAYSURF) + pygame.display.update() + if elem != "": + action(elem) + + return True + + +def main(): + """ + Main function + """ + host, port, teamName, logs = getArgs() + try: + ai = AI(host, port, teamName, logs) + + while ai.isRunning: + if ai.threads[0].is_alive() == False: + ai.isRunning = False + print("Player died", file=sys.stderr, flush=True) + ai.isRunning = handlingEvents(ai) + + if (len(ai.player.actions) > 0): + actionText.updateText("Actions: " + ai.player.actions[0].value) + else: + actionText.updateText("Actions: ") + + DISPLAYSURF.fill((0, 0, 0)) + food.draw(DISPLAYSURF) + linemate.draw(DISPLAYSURF) + deraumere.draw(DISPLAYSURF) + sibur.draw(DISPLAYSURF) + mendiane.draw(DISPLAYSURF) + phiras.draw(DISPLAYSURF) + thystame.draw(DISPLAYSURF) + actionText.draw(DISPLAYSURF) + pygame.display.update() + + pygame.quit() + sys.exit(0) + except KeyboardInterrupt: + ai.close() + sys.exit(0) + except Exception as e: + print(e, file=sys.stderr) + sys.exit(84) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(e, file=sys.stderr) + sys.exit(84) + sys.exit(0) diff --git a/bonus/api-ai/README.md b/bonus/api-ai/README.md index b9a110bf..ec623a93 100644 --- a/bonus/api-ai/README.md +++ b/bonus/api-ai/README.md @@ -55,7 +55,7 @@ source venv/bin/activate pip install -r requirements.txt ``` -6. Start the FastAPI server: +6. Start the FastAPI server (you need to run a server first): ```bash fastapi run ./src/main.py