import asyncio import json import logging from threading import Thread import websockets from websockets import WebSocketServerProtocol from websockets.exceptions import ConnectionClosedOK from hopper.models.ws_dto import GameStateDto from settings import settings class WSServer(Thread): def __init__(self, *args, **kwargs) -> None: self.connected_clients = set[WebSocketServerProtocol]() super().__init__(*args, **kwargs) async def handler(self, websocket: WebSocketServerProtocol) -> None: self.connected_clients.add(websocket) logging.info(f"Add client: {websocket.id}") try: await self.send_game_state_to_client(websocket) connected = True while connected: try: message = await websocket.recv() except ConnectionClosedOK: connected = False finally: self.connected_clients.remove(websocket) logging.info(f"Remove client: {websocket.id}") def _create_game_state_message(self) -> str: # avoid circular imports from hopper.api.dependencies import get_game_engine engine = get_game_engine() game_state = GameStateDto( board=engine.board, destination=engine.board.destination, players=engine.players, layers=engine.get_board_layout().layers, ) return json.dumps(game_state.dict()) async def send_game_state_to_client( self, websocket: WebSocketServerProtocol ) -> None: message = self._create_game_state_message() logging.debug(f"Sending game state to client: {websocket.id}") await websocket.send(message) async def send_game_state(self) -> None: if not self.connected_clients: return message = self._create_game_state_message() logging.debug( f"Sending game state to clients: {self.connected_clients}: {message}" ) for client in self.connected_clients: await client.send(message) async def run_async(self) -> None: logging.info( f"Starting FairHopper Websockets Server on {settings.ws_server.HOST}:{settings.ws_server.PORT}" ) async with websockets.serve( ws_handler=self.handler, host=settings.ws_server.HOST, port=settings.ws_server.PORT, ): await asyncio.Future() # run forever def run(self) -> None: asyncio.run(self.run_async())