diff --git a/README.md b/README.md index a1ad800..824dde0 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/socha?label=Python)](https://pypi.org/project/socha/) [![Discord](https://img.shields.io/discord/233577109363097601?color=blue&label=Discord)](https://discord.gg/ARZamDptG5) [![Documentation](https://img.shields.io/badge/Software--Challenge%20-Documentation-%234299e1)](https://docs.software-challenge.de/) -> Please note that this is a very early version, which may still contain some bugs. However, the client is able to play -> a game from start to end. + +> Please read the [documentation for this client](https://software-challenge-python-client.readthedocs.io/en/latest/) +> before you asking questions or opening an issue. This repository contains the Python package for the [Software-Challenge Germany](https://www.software-challenge.de), a programming competition for students. The students @@ -31,6 +32,8 @@ which installs the packages inside the folder. > Pleas make sure that you have at least **Python 3.6** installed. > Check with `$ python -V` or `$ python3 -V`. +> +> If not present you can install python with the following commands: > - Windows: `> winget install -e --id Python.Python.3.6` > - Debian: `$ sudo apt install python3.6` > - Arch: `$ sudo pacman -S python` @@ -173,9 +176,9 @@ you should have a directory structure that looks something like this: ```` my_player/ -|- venv/ -|- logic.py -|- start.sh +├── venv/ +├── logic.py +└── start.sh ```` The `my_player` directory, diff --git a/pyproject.toml b/pyproject.toml index 4d066b8..5b732f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socha" -version = "0.9.8" +version = "0.9.9" authors = [ { name = "FalconsSky", email = "stu222782@mail.uni-kiel.de" }, ] diff --git a/setup.py b/setup.py index 165bef2..5e5e803 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='socha', - version='0.9.8', + version='0.9.9', packages=['socha', 'socha.api', 'socha.api.plugin', 'socha.api.protocol', 'socha.api.networking'], url='https://github.com/FalconsSky/Software-Challenge-Python-Client', diff --git a/socha/api/networking/_network_interface.py b/socha/api/networking/_network_interface.py index 82196a2..f7ddd35 100644 --- a/socha/api/networking/_network_interface.py +++ b/socha/api/networking/_network_interface.py @@ -9,10 +9,10 @@ class _NetworkInterface: """ - This interface handels all package transfers. It'll send and _receive data from a given connection. + This interface handels all package transfers. It'll _send and _receive data from a given connection. """ - def __init__(self, host="localhost", port=13050, timeout=5): + def __init__(self, host="localhost", port=13050, timeout=10): """ :param host: Host of the server. Default is localhost. :param port: Port of the server. Default is 13050. @@ -20,16 +20,18 @@ def __init__(self, host="localhost", port=13050, timeout=5): """ self.host = host self.port = port + self.timeout = timeout self.connected: bool = False - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(timeout) + self.socket = None self.buffer: bytes = b"" def connect(self): """ - Connects the socket to the server and will be ready to listen for and send data. + Connects the socket to the server and will be ready to listen for and _send data. """ + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(self.timeout) self.socket.connect((self.host, self.port)) self.connected = True logging.info("Connected to server.") @@ -45,7 +47,7 @@ def close(self): def send(self, data: bytes): """ Sends the data to the server. It puts the data in the sending queue and the _SocketHandler thread will get - and send it. + and _send it. :param data: The data that is being sent as string. """ self.socket.sendall(data) diff --git a/socha/api/networking/_xflux.py b/socha/api/networking/_xflux.py index bf7ee17..7028f2d 100644 --- a/socha/api/networking/_xflux.py +++ b/socha/api/networking/_xflux.py @@ -2,7 +2,6 @@ Here are all incoming byte streams and all outgoing protocol objects handelt. """ import logging -import sys from xsdata.formats.dataclass.context import XmlContext from xsdata.formats.dataclass.parsers import XmlParser @@ -89,40 +88,11 @@ def __init__(self, host: str, port: int): :param host: Host of the server. :param port: Port of the server. """ - self.network_interface = _NetworkInterface(host, port) + self._network_interface = _NetworkInterface(host, port) self.connect_to_server() - self.x_flux = _XFlux() - self.running = False - self.first_time = True - - def start(self): - """ - Starts the client loop. - """ - self.running = True - self._client_loop() - - def _client_loop(self): - """ - The client loop. - This is the main loop, - where the client waits for messages from the server - and handles them accordingly. - """ - while self.running: - response = self._receive() - if isinstance(response, ProtocolPacket): - if isinstance(response, Left): - logging.info("The server left. Shutting down...") - self.handle_disconnect() - else: - logging.debug(f"Received new object: {response}") - self.on_object(response) - elif self.running: - logging.error(f"Received object of unknown class: {response}") - raise NotImplementedError("Received object of unknown class.") - logging.info("Done.") - sys.exit() + self._x_flux = _XFlux() + self._running = False + self._first_time = True def _receive(self): """ @@ -130,55 +100,34 @@ def _receive(self): :return: The next object in the stream. """ try: - receiving = self.network_interface.receive() - cls = self.x_flux.deserialize_object(receiving) + receiving = self._network_interface.receive() + cls = self._x_flux.deserialize_object(receiving) return cls except OSError: logging.error("Shutting down abnormally...") - self.running = False + self._running = False - def send(self, obj: ProtocolPacket): + def _send(self, obj: ProtocolPacket): """ Sends an object to the server. - :param obj: The object to send. + :param obj: The object to _send. """ - shipment = self.x_flux.serialize_object(obj) - if self.first_time: + shipment = self._x_flux.serialize_object(obj) + if self._first_time: shipment = "".encode("utf-8") + shipment - self.first_time = False - self.network_interface.send(shipment) + self._first_time = False + self._network_interface.send(shipment) def connect_to_server(self): """ Creates a TCP connection with the server. """ - self.network_interface.connect() + self._network_interface.connect() def close_connection(self): """ Sends a closing xml to the server and closes the connection afterwards. """ - close_xml = self.x_flux.serialize_object(Close()) - self.network_interface.send(close_xml) - self.network_interface.close() - - def handle_disconnect(self): - """ - Closes the connection and stops the client loop. - """ - self.close_connection() - self.running = False - - def on_object(self, message): - """ - Handles an object received from the server. - :param message: The object to handle. - """ - - def stop(self): - """ - Disconnects from the server and stops the client loop. - """ - if self.network_interface.connected: - self.close_connection() - self.running = False + self._send(Close()) + self._first_time = True + self._network_interface.close() diff --git a/socha/api/networking/player_client.py b/socha/api/networking/player_client.py index 2f3143d..ee67138 100644 --- a/socha/api/networking/player_client.py +++ b/socha/api/networking/player_client.py @@ -2,6 +2,7 @@ This module handels the communication with the api and the students logic. """ import logging +import sys import time from typing import List, Union @@ -9,7 +10,8 @@ from socha.api.plugin import penguins from socha.api.plugin.penguins import Field, GameState, Move, CartesianCoordinate from socha.api.protocol.protocol import State, Board, Data, \ - Error, From, Join, Joined, JoinPrepared, JoinRoom, To, Team, Room, Result, MoveRequest, ObservableRoomMessage + Error, From, Join, Joined, JoinPrepared, JoinRoom, To, Team, Room, Result, MoveRequest, ObservableRoomMessage, Left +from socha.api.protocol.protocol_packet import ProtocolPacket def _convertBoard(protocolBoard: Board) -> penguins.Board: @@ -37,13 +39,13 @@ def calculate_move(self) -> Move: def on_update(self, state: GameState): """ - If the server send a update on the current state of the game this method is called. + If the server _send a update on the current state of the game this method is called. :param state: The current state that server sent. """ def on_game_over(self, roomMessage: Result): """ - If the game has ended the server will send a result message. + If the game has ended the server will _send a result message. This method will called if this happens. :param roomMessage: The Result the server has sent. @@ -53,7 +55,7 @@ def on_error(self, logMessage: str): """ If error occurs, for instance when the logic sent a move that is not rule conform, - the server will send an error message and closes the connection. + the server will _send an error message and closes the connection. If this happens, this method is called. :param logMessage: The message, that server sent. @@ -88,42 +90,53 @@ def on_game_observed(self, message): :param message: The message that server sends with the response. """ + def on_game_left(self): + """ + If the server left the room, this method will be called. + If the client is running on survive mode it'll be running until shut downed manually. + """ + + def while_disconnected(self, player_client: '_PlayerClient'): + """ + The client loop will keep calling this method while there is no active connection to a game server. + This can be used to do tasks after a game is finished and the server left. + Please be aware, that the client has to be shut down manually if it is in survive mode. + The return statement is used to tell the client whether to exit or not. + + :type player_client: The player client in which the logic is integrated. + :return: True if the client should shut down. False if the client should continue to run. + """ + class _PlayerClient(_XFluxClient): """ The PlayerClient handles all incoming and outgoing objects accordingly to their types. """ - def __init__(self, host: str, port: int, handler: IClientHandler, keep_alive: bool): + def __init__(self, host: str, port: int, handler: IClientHandler, survive: bool): super().__init__(host, port) - self.game_handler = handler - self.keep_alive = keep_alive - - def authenticate(self, password: str, consumer): - ... - - def observe_room(self, room_id: str, observer): - ... + self._game_handler = handler + self.survive = survive def join_game(self): - super().send(Join()) + super()._send(Join()) def join_game_room(self, room_id: str): - super().send(JoinRoom(room_id=room_id)) + super()._send(JoinRoom(room_id=room_id)) def join_game_with_reservation(self, reservation: str): - super().send(JoinPrepared(reservation_code=reservation)) + super()._send(JoinPrepared(reservation_code=reservation)) def send_message_to_room(self, room_id: str, message): - super().send(Room(room_id=room_id, data=message)) + super()._send(Room(room_id=room_id, data=message)) - def on_object(self, message): + def _on_object(self, message): if isinstance(message, Room): room_id: str = message.room_id data = message.data.class_binding if isinstance(data, MoveRequest): start_time = time.time() - response = self.game_handler.calculate_move() + response = self._game_handler.calculate_move() logging.info(f"Sent {response} after {time.time() - start_time} seconds.") if response: from_value = None @@ -133,21 +146,67 @@ def on_object(self, message): response = Data(class_value="move", from_value=from_value, to=to) self.send_message_to_room(room_id, response) if isinstance(data, ObservableRoomMessage): - # TODO Set observer data if isinstance(data, State): game_state = GameState(turn=data.turn, start_team=Team(data.start_team), board=_convertBoard(data.board), last_move=data.last_move, fishes=penguins.Fishes(data.fishes.int_value[0], data.fishes.int_value[1])) - self.game_handler.history.append(game_state) - self.game_handler.on_update(game_state) + self._game_handler.history.append(game_state) + self._game_handler.on_update(game_state) elif isinstance(data, Result): - self.game_handler.history.append(data) - self.game_handler.on_game_over(data) + self._game_handler.history.append(data) + self._game_handler.on_game_over(data) if isinstance(data, Error): logging.error(data.message) - self.game_handler.history.append(data) - self.game_handler.on_error(data.message) + self._game_handler.history.append(data) + self._game_handler.on_error(data.message) else: - self.game_handler.on_room_message(data) + self._game_handler.on_room_message(data) elif isinstance(message, Joined): - self.game_handler.on_game_joined(room_id=message.room_id) + self._game_handler.on_game_joined(room_id=message.room_id) + elif isinstance(message, Left): + self._game_handler.on_game_left() + + def start(self): + """ + Starts the client loop. + """ + self._running = True + self._client_loop() + + def _client_loop(self): + """ + The client loop is the main loop, where the client waits for messages from the server + and handles them accordingly. + """ + while self._running: + if self._network_interface.connected: + response = self._receive() + if isinstance(response, ProtocolPacket): + if isinstance(response, Left): + if not self.survive: + logging.info("The server left.") + self.stop() + else: + logging.info("The server left. Client is in survive mode and keeps running.\n" + "Please shutdown the client manually.") + self.close_connection() + else: + logging.debug(f"Received new object: {response}") + self._on_object(response) + elif self._running: + logging.error(f"Received object of unknown class: {response}") + raise NotImplementedError("Received object of unknown class.") + else: + self._game_handler.while_disconnected(player_client=self) + + logging.info("Done.") + sys.exit() + + def stop(self): + """ + Disconnects from the server and stops the client loop. + """ + logging.info("Shutting down...") + if self._network_interface.connected: + self.close_connection() + self._running = False diff --git a/socha/api/protocol/protocol.py b/socha/api/protocol/protocol.py index 22b9a34..f6ed995 100644 --- a/socha/api/protocol/protocol.py +++ b/socha/api/protocol/protocol.py @@ -10,7 +10,7 @@ @dataclass class Left(ProtocolPacket): """ - If the game is over the server will send this message to the clients and closes the connection afterwards. + If the game is over the server will _send this message to the clients and closes the connection afterwards. """ class Meta: @@ -28,7 +28,7 @@ class Meta: @dataclass class MoveRequest(RoomMessage): """ - Request a client to send a Move. + Request a client to _send a Move. """ @@ -38,7 +38,7 @@ class Close(ProtocolPacket): Is sent by one party immediately before this party closes the communication connection and should make the receiving party also close the connection. - This should not be sent manually, the XFluxClient will automatically send it when stopped. + This should not be sent manually, the XFluxClient will automatically _send it when stopped. """ class Meta: @@ -48,7 +48,7 @@ class Meta: @dataclass class Authenticate(AdminLobbyRequest): """ - Authenticates a client as administrator to send AdminLobbyRequest`s. \n + Authenticates a client as administrator to _send AdminLobbyRequest`s. \n *Is not answered if successful.* """ @@ -183,7 +183,7 @@ class Meta: class Step(AdminLobbyRequest): """ When the client is authenticated as administrator, - it can send this step request to the server to advance the game for one move. + it can _send this step request to the server to advance the game for one move. This is not possible if the game is not paused. """ @@ -203,7 +203,7 @@ class Meta: class Prepare(AdminLobbyRequest): """ When the client is authenticated as administrator, - it can send this request to prepare the room for the game. + it can _send this request to prepare the room for the game. """ class Meta: @@ -518,7 +518,7 @@ class Meta: @dataclass class Entry: """ - Is send when a game is won by one of the players. + Is _send when a game is won by one of the players. This element contains the winning player and the score of the player. """ @@ -744,7 +744,7 @@ class Meta: class Room(ProtocolPacket): """ The root element of every room packet. - It contains a data element when send that contains the actual data, + It contains a data element when _send that contains the actual data, that are needed for the game to work. """ @@ -779,7 +779,7 @@ class WelcomeMessage(RoomOrchestrationMessage): class Result(ObservableRoomMessage): """ Result of a game. - This will the server send after a game is finished. + This will the server _send after a game is finished. """ definition: Definition scores: Scores diff --git a/socha/starter.py b/socha/starter.py index cd13443..6faad19 100644 --- a/socha/starter.py +++ b/socha/starter.py @@ -36,7 +36,7 @@ def __init__(self, logic: IClientHandler, host: str = "localhost", port: int = 1 self.port: int = args.port or port self.reservation: str = args.reservation or reservation self.room_id: str = args.room or room_id - self.keep_alive: bool = args.survive or survive + self.survive: bool = args.survive or survive self.write_log: bool = args.log or log if args.verbose or verbose: @@ -53,7 +53,7 @@ def __init__(self, logic: IClientHandler, host: str = "localhost", port: int = 1 logging.basicConfig(level=level, format="%(asctime)s: %(levelname)s - %(message)s") logging.info("Starting...") - self.client = _PlayerClient(host=self.host, port=self.port, handler=logic, keep_alive=self.keep_alive) + self.client = _PlayerClient(host=self.host, port=self.port, handler=logic, survive=self.survive) if reservation: self.client.join_game_with_reservation(reservation)