diff --git a/hopper/api_tests/requests.http b/api_tests/requests.http similarity index 100% rename from hopper/api_tests/requests.http rename to api_tests/requests.http diff --git a/hopper/api/views.py b/hopper/api/views.py index 918e340..e6a31cf 100644 --- a/hopper/api/views.py +++ b/hopper/api/views.py @@ -53,7 +53,9 @@ async def get_game_info( ) -@router.post("/game", response_model=StartGameResponseDto) +@router.post( + "/game", response_model=StartGameResponseDto, status_code=status.HTTP_201_CREATED +) async def start_game( body: StartGameRequestDto, engine: GameEngine = Depends(get_game_engine), @@ -72,7 +74,6 @@ async def start_game( @router.get( "/player/{uuid}", response_model=PlayerInfoResponseDto, - status_code=status.HTTP_201_CREATED, responses={ status.HTTP_403_FORBIDDEN: { "model": ErrorResponseDto, diff --git a/hopper/engine.py b/hopper/engine.py index 03b4f8f..a0c9cc0 100644 --- a/hopper/engine.py +++ b/hopper/engine.py @@ -5,6 +5,7 @@ from typing import Optional from hopper.enums import Direction, PlayerMoveResult from hopper.errors import Collision, PositionOutOfBounds +from hopper.interfaces import SendGameStateInterface from hopper.models.board import ( BOARD_DUMP_CHARS, BoardLayout, @@ -17,12 +18,13 @@ from hopper.models.board import ( ) from hopper.models.player import Player, PlayerList, Position from hopper.watchdog import InactivityWatchdog -from hopper.ws_server import WSServer from settings import settings class GameEngine: - def __init__(self, board: GameBoard, ws_server: Optional[WSServer] = None) -> None: + def __init__( + self, board: GameBoard, ws_server: Optional[SendGameStateInterface] = None + ) -> None: self.board = board self.ws_server = ws_server self.players = PlayerList() @@ -163,7 +165,7 @@ class GameEngineFactory: board_width: int, board_height: int, obstacle_count: int = 0, - ws_server: Optional[WSServer] = None, + ws_server: Optional[SendGameStateInterface] = None, ) -> GameEngine: board = GameBoard( width=board_width, @@ -188,7 +190,9 @@ class GameEngineFactory: return game @staticmethod - def create_default(ws_server: Optional[WSServer] = None) -> GameEngine: + def create_default( + ws_server: Optional[SendGameStateInterface] = None, + ) -> GameEngine: return GameEngineFactory.create( board_width=settings.board.WIDTH, board_height=settings.board.HEIGHT, diff --git a/hopper/interfaces.py b/hopper/interfaces.py new file mode 100644 index 0000000..4a5a0e1 --- /dev/null +++ b/hopper/interfaces.py @@ -0,0 +1,6 @@ +from typing import Protocol + + +class SendGameStateInterface(Protocol): + async def send_game_state(self) -> None: + ... diff --git a/hopper/watchdog.py b/hopper/watchdog.py index d6df4f2..9bf59b8 100644 --- a/hopper/watchdog.py +++ b/hopper/watchdog.py @@ -5,14 +5,18 @@ import time from threading import Thread from typing import Optional +from hopper.interfaces import SendGameStateInterface from hopper.models.player import PlayerList -from hopper.ws_server import WSServer from settings import settings class InactivityWatchdog(Thread): def __init__( - self, players: PlayerList, ws_server: Optional[WSServer] = None, *args, **kwargs + self, + players: PlayerList, + ws_server: Optional[SendGameStateInterface] = None, + *args, + **kwargs, ) -> None: self.players = players self.ws_server = ws_server diff --git a/hopper/ws_server.py b/hopper/ws_server.py index e87866f..43ef659 100644 --- a/hopper/ws_server.py +++ b/hopper/ws_server.py @@ -12,20 +12,20 @@ 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: + """New handler instance spawns for each connected client""" self.connected_clients.add(websocket) logging.info(f"Add client: {websocket.id}") try: + # send initial game state to connected client await self.send_game_state_to_client(websocket) + # loop and do nothing while client is connected connected = True while connected: try: - message = await websocket.recv() + # we're expecting nothing from client, but read if client sends a message + await websocket.recv() except ConnectionClosedOK: connected = False finally: @@ -49,11 +49,13 @@ class WSServer(Thread): async def send_game_state_to_client( self, websocket: WebSocketServerProtocol ) -> None: + """Send game state to the client""" 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: + """Broadcast game state to all connected clients""" if not self.connected_clients: return @@ -77,4 +79,5 @@ class WSServer(Thread): await asyncio.Future() # run forever def run(self) -> None: + self.connected_clients = set[WebSocketServerProtocol]() asyncio.run(self.run_async())