예제 #1
0
 async def run_game_loop(self, interval: float = 0.02) -> None:  # pylint: disable=function-redefined
     # pylint: disable=missing-docstring
     if self._game_state_store.get_game_state(
     ).game_status == GameStatus.get("Paused"):
         self._game_state_store.push_update(
             GameStateUpdate(
                 self._game_state_store.get_game_state().time_order + 1,
                 game_status=GameStatus.get("Active")))
     game_state = self._game_state_store.get_game_state()
     dt = interval
     self._game_loop_is_running = True
     logger.info(
         f"State machine starting game loop with interval of {interval} seconds."
     )
     while game_state.game_status == GameStatus.get("Active"):
         t0 = time.time()
         update_dict = self.time_step(game_state, dt)
         while not self._event_queue.empty():
             event = await self._event_queue.get()
             event_update = await self._universal_event_handler.handle(
                 event, game_state=game_state, dt=dt)
             update_dict.update(event_update)
             if time.time() - t0 > 0.95 * interval:
                 break
         self._game_state_store.push_update(
             GameStateUpdate(game_state.time_order + 1, **update_dict))
         game_state = self._game_state_store.get_game_state()
         dt = max(interval, time.time() - t0)
         await curio.sleep(max(0, interval - dt))
         self.game_time += dt
     logger.info("Game loop stopped.")
     self._game_loop_is_running = False
예제 #2
0
    def run(self, hostname: str, port: int):
        """Run state machine and server and bind the server to a given address.

        # Arguments
        hostname (str): hostname or IPv4 address the server will be bound to
        port (int): port number the server will be bound to

        """
        self.game_state_machine.run_game_loop_in_thread()
        self.server.run(port, hostname, self.game_state_machine)
        self.game_state_machine.stop()
        logger.info("Backend successfully shut down.")
예제 #3
0
 def __init__(self,
              initial_game_state: GameState,
              time_step_function,
              event_handlers: dict = None):
     logger.info("Assembling Backend ...")
     self.game_state_store = GameStateStore(initial_game_state)
     self.game_state_machine = GameStateMachine(self.game_state_store)
     setattr(self.game_state_machine, "time_step", time_step_function)
     if event_handlers is not None:
         for event_type, handler_function in event_handlers.items():
             self.game_state_machine.register_event_handler(
                 event_type, handler_function)
     self.server = Server(self.game_state_store)
     logger.info("Backend assembled and ready.")
예제 #4
0
 async def stop(self, timeout: float = 1.0) -> bool:  # pylint: disable=function-redefined
     # pylint: disable=missing-docstring
     logger.info("Trying to stop game loop ...")
     if self._game_state_store.get_game_state(
     ).game_status == GameStatus.get("Active"):
         self._game_state_store.push_update(
             GameStateUpdate(
                 self._game_state_store.get_game_state().time_order + 1,
                 game_status=GameStatus.get("Paused")))
     t0 = time.time()
     while self._game_loop_is_running:
         if time.time() - t0 > timeout:
             break
         await curio.sleep(0)
     return not self._game_loop_is_running
예제 #5
0
    def register_event_handler(self, event_type: str,
                               event_handler_function) -> None:
        """Register an event handler for a specific event type.

        # Arguments
        event_type (str): string that identifies the events to be handled by this function
        event_handler_function (callable, coroutine): callback function or coroutine that will be invoked
        with the handler args and kwargs with which the incoming event has been dispatched

        # Raises
        TypeError: if `event_handler_function` is not callable

        """
        logger.info(
            f"Registering event handler for events of type {event_type}.")
        if not callable(event_handler_function):
            raise TypeError(
                f"'{event_handler_function.__class__.__name__}' object is not callable."
            )
        self._event_handlers[event_type] = event_handler_function
예제 #6
0
 async def loop(self):  # pylint: disable=function-redefined
     # pylint: disable=missing-docstring
     logger.info(f"Trying to connect to server ...")
     self._set_status("Connecting")
     async with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
         send_loop_task = await curio.spawn(self._send_loop, sock)
         recv_loop_task = await curio.spawn(self._client_recv_loop, sock)
         event_loop_task = await curio.spawn(self._event_loop)
         # check for disconnect event
         while not self.status == ConnectionStatus.get("Disconnected"):
             command = await self._command_queue.get()
             if command == "shutdown":
                 logger.info(
                     f"Sending shutdown command to server at {self.remote_address}."
                 )
                 await sock.sendto("shutdown".encode("utf-8"),
                                   self.remote_address)
                 break
             elif command == "shut_me_down":
                 break
         logger.info(f"Shutting down connection to {self.remote_address}.")
         await recv_loop_task.cancel()
         await send_loop_task.cancel()
         await event_loop_task.cancel()
         self._set_status("Disconnected")
예제 #7
0
 def _throttling_state_machine(self, t: int, state: dict):
     """Calculate a new state for congestion avoidance."""
     if self.quality == "good":
         if self.latency > self._latency_threshold:  # switch to bad mode
             logger.warning((
                 f"Throttling down connection to {self.remote_address} because "
                 f"latency ({self.latency}) is above latency threshold ({self._latency_threshold})."
             ))
             self.quality = "bad"
             self._package_interval = self._package_intervals["bad"]
             logger.debug(
                 f"new package interval: {self._package_interval} seconds.")
             # if good conditions didn't last at least the throttle time, increase it
             if t - state["last_quality_change"] < state["throttle_time"]:
                 state["throttle_time"] = min([
                     state["throttle_time"] * 2.0, self._max_throttle_time
                 ])
             state["last_quality_change"] = t
         # if good conditions lasted throttle time since last milestone
         elif t - state["last_good_quality_milestone"] > state[
                 "throttle_time"]:
             if self._package_interval > self._package_intervals["good"]:
                 logger.info((
                     f"Throttling up connection to {self.remote_address} because latency ({self.latency}) "
                     f"has been below latency threshold ({self._latency_threshold}) "
                     f"for {state['throttle_time']} seconds."))
                 self._package_interval = self._package_intervals["good"]
                 logger.debug(
                     f"new package interval: {self._package_interval} seconds."
                 )
             state["throttle_time"] = max(
                 [state["throttle_time"] / 2.0, self._min_throttle_time])
             state["last_good_quality_milestone"] = t
     else:  # self.quality == 'bad'
         if self.latency < self._latency_threshold:  # switch to good mode
             self.quality = "good"
             state["last_quality_change"] = t
             state["last_good_quality_milestone"] = t
예제 #8
0
    async def loop(cls, hostname: str, port: int, server, event_wire) -> None:
        """Continously orchestrate and operate connections to clients.

        This coroutine will keep listening for client packages, create new #ServerConnection objects
        when necessary and make sure all packages are handled by and sent via the right connection.

        It will return as soon as the server receives a shutdown message.

        # Arguments
        hostname (str): the hostname or IPv4 address to which to bind the server socket
        port (int): the port number to which to bind the server socket
        server (pygase.Server): the server for which this loop is run
        event_wire (pygase.GameStateMachine): object to which events are to be repeated
           (has to implement a `_push_event` method)

        """
        logger.info(f"Trying to run server on {(hostname, port)} ...")
        async with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
            sock.bind((hostname, port))
            server._hostname, server._port = sock.getsockname()  # pylint: disable=protected-access
            connection_tasks = curio.TaskGroup()
            logger.info(
                f"Server successfully started and listening to packages from clients on {(hostname, port)}."
            )
            while True:
                data, client_address = await sock.recvfrom(Package._max_size)  # pylint: disable=protected-access
                try:
                    package = ClientPackage.from_datagram(data)
                    # Create new connection if client is unknown.
                    if not client_address in server.connections:
                        logger.info(
                            f"New client connection from {client_address}.")
                        new_connection = cls(
                            client_address,
                            server._universal_event_handler,  # pylint: disable=protected-access
                            server.game_state_store,
                            package.time_order,
                            event_wire,
                        )
                        await connection_tasks.spawn(
                            new_connection._send_loop,
                            sock  # pylint: disable=protected-access
                        )
                        await connection_tasks.spawn(new_connection._event_loop
                                                     )  # pylint: disable=protected-access
                        # For now, the first client connection becomes host.
                        if server.host_client is None:
                            logger.info(
                                f"Setting {client_address} as client with host permissions."
                            )
                            server.host_client = client_address
                        server.connections[client_address] = new_connection
                    elif server.connections[
                            client_address].status == ConnectionStatus.get(
                                "Disconnected"):
                        # Start sending packages again, which will also set status to "Connected".
                        logger.info(
                            f"Client reconnecting from {client_address}.")
                        await connection_tasks.spawn(
                            server.connections[client_address]._send_loop,
                            sock  # pylint: disable=protected-access
                        )
                    for event in package.events:
                        event.handler_kwargs["client_address"] = client_address
                    await server.connections[client_address]._recv(package)  # pylint: disable=protected-access
                except ProtocolIDMismatchError:
                    # ignore all non-PyGaSe packages
                    try:
                        if data.decode(
                                "utf-8"
                        ) == "shutdown" and client_address == server.host_client:
                            logger.info(
                                f"Received shutdown command from host client {client_address}."
                            )
                            break
                        elif data.decode("utf-8") == "shut_me_down":
                            break
                        else:
                            logger.warning("Received unknown package.")
                    except UnicodeDecodeError:
                        logger.warning("Received unknown package.")
            logger.info(f"Shutting down server on {(hostname, port)}.")
            await connection_tasks.cancel_remaining()
예제 #9
0
 def _set_status(self, status: str):
     """Set `self.status` to a new #ConnectionStatus value."""
     self.status = ConnectionStatus.get(status)
     logger.info(
         f"Status of connection to {self.remote_address} set to '{status}'."
     )