Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "socha"
version = "1.0.7"
version = "1.0.8"
authors = [
{ name = "FalconsSky", email = "stu222782@mail.uni-kiel.de" },
]
Expand Down
150 changes: 115 additions & 35 deletions socha/api/networking/game_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""
This module handels the communication with the api and the students logic.
This module handles the communication with the api and the students' logic.
"""
import gc
import logging
import sys
import threading
import time
from typing import List, Union

Expand All @@ -12,7 +13,7 @@
from socha.api.plugin.penguins.game_state import GameState
from socha.api.plugin.penguins.utils import handle_move, if_last_game_state, if_not_last_game_state
from socha.api.protocol.protocol import State, Error, Join, Joined, JoinPrepared, JoinRoom, Room, Result, MoveRequest, \
Left, Errorpacket
Left, Errorpacket, Authenticate, Prepared, Slot, Prepare, Observe, Cancel, Observed, Step, Pause
from socha.api.protocol.protocol_packet import ProtocolPacket


Expand All @@ -24,21 +25,21 @@ def calculate_move(self) -> Move:
Calculates a move that the logic wants the server to perform in the game room.
"""

def on_update(self, state: GameState):
def on_update(self, state: GameState) -> None:
"""
If the server _send a update on the current state of the game this method is called.
If the server _send an 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):
def on_game_over(self, roomMessage: Result) -> None:
"""
If the game has ended the server will _send a result message.
This method will called if this happens.
This method will call if this happens.

:param roomMessage: The Result the server has sent.
"""

def on_error(self, logMessage: str):
def on_error(self, logMessage: str) -> None:
"""
If error occurs,
for instance when the logic sent a move that is not rule conform,
Expand All @@ -48,78 +49,131 @@ def on_error(self, logMessage: str):
:param logMessage: The message, that server sent.
"""

def on_room_message(self, data):
def on_room_message(self, data) -> None:
"""
If the server sends a message that cannot be handelt by anny other method,
If the server sends a message that cannot be handheld by anny other method,
this will be called.

:param data: The data the Server sent.
"""

def on_game_prepared(self, message):
def on_game_prepared(self, message) -> None:
"""
If the game has been prepared by the server this method will be called.

:param message: The message that server sends with the response.
"""

def on_game_joined(self, room_id):
def on_game_joined(self, room_id) -> None:
"""
If the client has successfully joined a game room this method will be called.

:param room_id: The room id the client has joined.
"""

def on_game_observed(self, message):
"""
If the client successfully joined as observer this method will be called.

:param message: The message that server sends with the response.
"""

def on_game_left(self):
def on_game_left(self) -> None:
"""
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: 'GameClient'):
def while_disconnected(self, player_client: 'GameClient') -> None:
"""
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.
"""

def on_create_game(self, game_client: 'GameClient') -> None:
"""
This method will be called if the client is in admin mode and the client has authenticated with the server.
The client can now create a game.

:param game_client: The client that is in admin mode.
"""

def on_prepared(self, game_client: 'GameClient', room_id: str, reservations: List[str]) -> None:
"""
This method will be called if the client is in admin mode and the client has created a game.

:param game_client: The client that is in admin mode.
:param room_id: The room id of the game.
:param reservations: The reservations of the game.
"""

def on_observed(self, game_client: 'GameClient', room_id: str):
"""
This method will be called if the client is in admin mode and the client is observing a game.

:param game_client: The client that is in admin mode.
:param room_id: The room id of the game.
"""

def while_waiting(self) -> None:
"""
This method will be called while the client is waiting for the server to send a new message.
This method is running threaded, which will be terminated without warning if the client receives a new message.
"""


class GameClient(XMLProtocolInterface):
"""
The PlayerClient handles all incoming and outgoing objects accordingly to their types.
"""

def __init__(self, host: str, port: int, handler: IClientHandler, reservation: str,
room_id: str, auto_reconnect: bool, survive: bool):
room_id: str, password: str, auto_reconnect: bool, survive: bool, headless: bool):
super().__init__(host, port)
self._game_handler = handler
self.reservation = reservation
self.room_id = room_id
self.password = password
self.auto_reconnect = auto_reconnect
self.survive = survive
self.headless = headless

def join_game(self):
self._send(Join())
logging.info("Joining game")
self.send(Join())

def join_game_room(self, room_id: str):
self._send(JoinRoom(room_id=room_id))
logging.info(f"Joining game room '{room_id}'")
self.send(JoinRoom(room_id=room_id))

def join_game_with_reservation(self, reservation: str):
self._send(JoinPrepared(reservation_code=reservation))
logging.info(f"Joining game with reservation '{reservation}'")
self.send(JoinPrepared(reservation_code=reservation))

def authenticate(self, password: str):
logging.info(f"Authenticating with password '{password}'")
self.send(Authenticate(password=password))

def create_game(self, player_1: Slot, player_2: Slot, game_type: str, pause: bool):
logging.info(f"Creating game with {player_1}, {player_2} and game type '{game_type}'")
self.send(Prepare(game_type=game_type, pause=pause, slot=[player_1, player_2]))

def observe(self, room_id: str):
logging.info(f"Observing game room '{room_id}'")
self.send(Observe(room_id=room_id))

def cancel(self, room_id: str):
logging.info(f"Cancelling game room '{room_id}'")
self.send(Cancel(room_id=room_id))

def step(self, room_id: str):
logging.info(f"Stepping game room '{room_id}'")
self.send(Step(room_id=room_id))

def pause(self, room_id: str, pause: bool):
logging.info(f"Set pause of game room '{room_id}' to '{pause}'")
self.send(Pause(room_id=room_id, pause=pause))

def send_message_to_room(self, room_id: str, message):
self._send(Room(room_id=room_id, data=message))
logging.log(15, f"Sending message to room '{room_id}'")
self.send(Room(room_id=room_id, data=message))

def _on_object(self, message):
"""
Expand All @@ -135,21 +189,38 @@ def _on_object(self, message):
logging.error(f"An error occurred while handling the request: {message}")
self._game_handler.on_error(str(message))
self.stop()
else:
elif isinstance(message, Joined):
logging.log(15, f"Game joined received with room id '{message.room_id}'")
self._game_handler.on_game_joined(room_id=message.room_id)
elif isinstance(message, Left):
logging.log(15, f"Game left received with room id '{message.room_id}'")
self._game_handler.on_game_left()
elif isinstance(message, Prepared):
logging.log(15, f"Game prepared received with reservation '{message.reservation}'")
self._game_handler.on_prepared(game_client=self, room_id=message.room_id, reservations=message.reservation)
elif isinstance(message, Observed):
logging.log(15, f"Game observing received with room id '{message.room_id}'")
self._game_handler.on_observed(game_client=self, room_id=message.room_id)
elif isinstance(message, Room) and not self.headless:
room_id = message.room_id
if isinstance(message, Joined):
self._game_handler.on_game_joined(room_id=room_id)
elif isinstance(message, Left):
self._game_handler.on_game_left()
elif isinstance(message.data.class_binding, MoveRequest):

if isinstance(message.data.class_binding, MoveRequest):
logging.log(15, f"Move request received for room id '{room_id}'")
self._on_move_request(room_id)
elif isinstance(message.data.class_binding, State):
logging.log(15, f"State received for room id '{room_id}'")
self._on_state(message)
elif isinstance(message.data.class_binding, Result):
logging.log(15, f"Result received for room id '{room_id}'")
self._game_handler.history[-1].append(message.data.class_binding)
self._game_handler.on_game_over(message.data.class_binding)
elif isinstance(message, Room):
else:
logging.log(15, f"Room message received for room id '{room_id}'")
self._game_handler.on_room_message(message.data.class_binding)
else:
room_id = message.room_id
logging.log(15, f"Room message received for room id '{room_id}'")
self._game_handler.on_room_message(message)

def _on_move_request(self, room_id):
start_time = time.time()
Expand Down Expand Up @@ -184,12 +255,16 @@ def start(self):
def join(self) -> None:
"""
Tries to join a game with a connected server. It uses the reservation, or room id to join.
If their are not present it joins just without, as fallback.
If they are not present it joins just without, as fallback.
"""
if self.reservation:
self.join_game_with_reservation(self.reservation)
elif self.room_id:
self.join_game_room(self.room_id)
elif self.password:
self.authenticate(self.password)
self.first_time = False
self._game_handler.on_create_game(game_client=self)
else:
self.join_game()

Expand All @@ -198,7 +273,7 @@ def join(self) -> None:

def _handle_left(self):
self.first_time = True
self.disconnect()
self.network_interface.close()
if self.survive:
logging.info("The server left. Client is in survive mode and keeps running.\n"
"Please shutdown the client manually.")
Expand Down Expand Up @@ -227,18 +302,23 @@ 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_waiting = None
while self.running:
if self.network_interface.connected:
response = self._receive()
if not response:
continue
elif isinstance(response, ProtocolPacket):
logging.debug(f"Received new object: {response}")
if while_waiting:
while_waiting.join(timeout=0.0)
if isinstance(response, Left):
self._game_handler.on_game_left()
self._handle_left()
else:
self._on_object(response)
while_waiting = threading.Thread(target=self._game_handler.while_waiting)
while_waiting.start()
gc.collect()
elif self.running:
logging.error(f"Received a object of unknown class: {response}")
Expand Down
5 changes: 3 additions & 2 deletions socha/api/networking/network_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, host="localhost", port=13050, timeout=0.1):
host (str): The hostname or IP address of the server to connect to. Defaults to "localhost".
port (int): The port number to connect to on the server. Defaults to 13050.
timeout (float): The timeout for socket operations, in seconds. Defaults to 0.1.
connected (bool): Whether or not the socket is currently connected to the server.
connected (bool): Whether the socket is currently connected to the server.
socket (socket.socket): The underlying socket object.
buffer (bytes): A buffer for storing received data.
"""
Expand Down Expand Up @@ -60,7 +60,8 @@ def receive(self) -> Union[bytes, None]:

If a timeout occurs or a connection reset error is encountered, the socket is closed and None is returned.
"""
regex = re.compile(br"<((room[\s\S]+?</room>)|errorpacket[\s\S]+?</errorpacket>|.*?/>)")
regex = re.compile(
br"<((room[\s\S]+?</room>)|errorpacket[\s\S]+?</errorpacket>|prepared[\s\S]+?</prepared>|.*?/>)")
while True:
try:
chunk = self.socket.recv(16129)
Expand Down
21 changes: 11 additions & 10 deletions socha/api/networking/xml_protocol_interface.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Here are all incoming byte streams and all outgoing protocol objects handelt.
Here are all incoming byte streams and all outgoing protocol objects handheld.
"""
import contextlib
import logging
Expand All @@ -13,11 +13,11 @@
from xsdata.formats.dataclass.serializers.config import SerializerConfig

from socha.api.networking.network_socket import NetworkSocket
from socha.api.plugin.penguins.team import TeamEnum, Move
from socha.api.plugin.penguins.team import TeamEnum
from socha.api.protocol.protocol import *


def customClassFactory(clazz, params: dict):
def custom_class_factory(clazz, params: dict):
if clazz.__name__ == "Data":
try:
params.pop("class_binding")
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(self, host: str, port: int):
self.first_time = True

context = XmlContext()
deserialize_config = ParserConfig(class_factory=customClassFactory)
deserialize_config = ParserConfig(class_factory=custom_class_factory)
self.deserializer = XmlParser(handler=XmlEventHandler, context=context, config=deserialize_config)

serialize_config = SerializerConfig(pretty_print=True, xml_declaration=False)
Expand All @@ -73,9 +73,9 @@ def connect(self):

def disconnect(self):
"""
Sends a closing xml to the server and closes the connection afterwards.
Sends a closing xml to the server and closes the connection afterward.
"""
self._send(Close())
self.send(Close())
self.network_interface.close()

def _receive(self):
Expand All @@ -102,7 +102,7 @@ def _receive(self):
self.running = False
raise

def _send(self, obj: ProtocolPacket) -> None:
def send(self, obj: ProtocolPacket) -> None:
"""
Sends an object to the server.

Expand All @@ -121,6 +121,7 @@ def _send(self, obj: ProtocolPacket) -> None:
raise
else:
logging.debug("Sent shipment to server: %s", shipment)
self.first_time = False

@contextlib.contextmanager
def _encode_context(self) -> Iterator[Callable[[Any], bytes]]:
Expand All @@ -133,13 +134,13 @@ def encode(obj: Any) -> bytes:

yield encode

def _deserialize_object(self, byteStream: bytes) -> ProtocolPacket:
def _deserialize_object(self, byte_stream: bytes) -> ProtocolPacket:
"""
Deserialize a xml byte stream to a ProtocolPacket.
:param byteStream: The byte stream to deserialize.
:param byte_stream: The byte stream to deserialize.
:return: The deserialized ProtocolPacket child.
"""
return self.deserializer.from_bytes(byteStream)
return self.deserializer.from_bytes(byte_stream)

def _serialize_object(self, object_class: object) -> bytes:
"""
Expand Down
Loading