From 9151aa3e1ef85246e5c7054bb63c87edf751eb43 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Thu, 11 May 2023 15:08:24 +0200 Subject: [PATCH] Product selection message handler --- README.md | 59 ++++++++++++++++++++++++++--------------- fairhopper-sdk | 2 +- frontend/index.html | 10 ++++--- frontend/js/frontend.js | 20 +++++++++++++- hopper/engine.py | 37 ++++++++------------------ hopper/models/config.py | 1 - hopper/models/ws_dto.py | 19 +++++-------- hopper/ws_server.py | 54 +++++++++++++++++++++---------------- 8 files changed, 113 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 9483aac..b428d79 100644 --- a/README.md +++ b/README.md @@ -170,11 +170,11 @@ package Masterpiece #seashell { usecase Game as "Game Engine" usecase WS as "WS Server" } - usecase Vis as "Visualisation\nService" + usecase Vis as "Flutter\nVisualisation\nService" } -usecase ExtVis1 as "Visualisation\nService" -usecase ExtVis2 as "Visualisation\nService" +usecase ExtVis1 as "Visualisation\nClient" +usecase ExtVis2 as "Visualisation\nClient" P1 -left-> API: REST API P2 -left-> API: REST API @@ -204,41 +204,58 @@ Game ->o WS: Send initial state Client1 ->o WS: Client connect activate WS #coral WS -> Client1: Game state -deactivate +deactivate WS Client2 ->o WS: Client connect activate WS #coral WS -> Client2: Game state -deactivate +deactivate WS loop #lightyellow On game state change Game ->o WS: Game state activate WS #coral WS o-> Client1: Game state WS o-> Client2: Game state - deactivate + deactivate WS end -== Product purchase mode == +== Player reached destination == -Game -> WS: Purchase start -activate WS #coral - WS o-> Client1: Purchase start - WS o-> Client2: Purchase start -deactivate +Game -> Game: Lock game for other players +activate Game + Game -> WS: Player reached destination + activate WS #coral + WS o-> Client1: Select product + WS o-> Client2: Select product + deactivate WS +deactivate Game -loop #lightyellow Purchase countdown timer - Game ->o WS: Timer count down +loop #lightyellow Product select countdown timer (60s) + Game ->o WS: Timer timeout + activate Game activate WS #coral - WS o-> Client1: Purchase time left - WS o-> Client2: Purchase time left - deactivate + WS o-> Client1: Cancel selection + WS o-> Client2: Cancel selection + deactivate WS + Game -> Game: Unlock game + deactivate Game end -Game -> WS: Purchase done + +Client1 -> WS: Product selected activate WS #coral - WS o-> Client1: Purchase done - WS o-> Client2: Purchase done -deactivate + WS o-> Game: Product selected + activate Game + WS o-> Client2: Product selected +deactivate WS + + +Game -> Game: Unlock game + Game -> WS: Game state + activate WS #coral + WS o-> Client1: Game state + WS o-> Client2: Game state + deactivate WS +deactivate Game ``` diff --git a/fairhopper-sdk b/fairhopper-sdk index fd71fa2..ed8f93d 160000 --- a/fairhopper-sdk +++ b/fairhopper-sdk @@ -1 +1 @@ -Subproject commit fd71fa276c2f2030b60a6d41470641d80043618d +Subproject commit ed8f93d7d08112d4e8b625a6b22f9b3cdf4daea0 diff --git a/frontend/index.html b/frontend/index.html index 25ea775..c61962f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,9 +6,11 @@ + integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> - + @@ -21,7 +23,7 @@ FairHopper Visualisation Client
@@ -59,7 +61,7 @@ moves.
diff --git a/frontend/js/frontend.js b/frontend/js/frontend.js index 9caaa62..3d38c4e 100644 --- a/frontend/js/frontend.js +++ b/frontend/js/frontend.js @@ -2,6 +2,8 @@ if (typeof FAIRHOPPER_WS_SERVER === "undefined") { var FAIRHOPPER_WS_SERVER = "ws://127.0.0.1:8011"; } +let ws = null; + const BOARD_ICONS = { PLAYER: "😀", PLAYER_ON_DESTINATION: "😎", @@ -146,7 +148,7 @@ function productPurchaseDone(product) { function wsConnect() { console.log("Attempting to connect to", FAIRHOPPER_WS_SERVER); - let ws = new WebSocket(FAIRHOPPER_WS_SERVER); + ws = new WebSocket(FAIRHOPPER_WS_SERVER); ws.onopen = () => { console.log("WS connected"); @@ -178,6 +180,7 @@ function wsConnect() { }; ws.onclose = (e) => { + ws = null; setTimeout(() => { wsConnect(); }, 1000); @@ -189,6 +192,21 @@ function wsConnect() { }; } +function finishProductSelection() { + if (!ws) { + return; + } + const wsMessage = { + message: "product_selection_done", + data: null, + }; + ws.send(JSON.stringify(wsMessage)); +} + window.onload = () => { + document.getElementById("finish-product-selection").onclick = () => { + finishProductSelection(); + }; + wsConnect(); }; diff --git a/hopper/engine.py b/hopper/engine.py index 9fce5a4..201b1b8 100644 --- a/hopper/engine.py +++ b/hopper/engine.py @@ -5,11 +5,7 @@ from typing import Optional from hopper.countdown_timer import CountdownTimer from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState -from hopper.errors import ( - Collision, - GameLockForMovement, - PositionOutOfBounds, -) +from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds from hopper.models.board import ( BOARD_DUMP_CHARS, BoardLayout, @@ -179,24 +175,17 @@ class GameEngine: await self.ws_server.send_player_reached_destination_message(player=player) logging.info( - f"Starting purchase countdown timer for {settings.purchase_timeout} seconds" + f"Starting product selection countdown timer for {settings.purchase_timeout} seconds" ) def on_purchase_timer_tick(time_left) -> None: - logging.info(f"Purchase countdown timer tick, time left: {time_left}") - asyncio.run( - self.ws_server.send_product_purchase_time_left_message( - player=player, time_left=time_left - ) - ) + logging.info(f"Product selection countdown timer tick, time left: {time_left}") def on_purchase_timer_done() -> None: - logging.info("Ding ding! Purchase countdown timer timeout") + logging.info("Ding ding! Product selection countdown timer timeout") self._purchase_countdown_timer = None asyncio.run( - self.ws_server.send_product_purchase_done_message( - player=player, product=None - ) + self.ws_server.send_product_selection_done_message() ) self.game_state = GameState.RUNNING asyncio.run(self.send_game_dump()) @@ -210,16 +199,12 @@ class GameEngine: await asyncio.sleep(settings.game.PURCHASE_START_DELAY) - # async def purchase_product(self, player: Player, product: Product) -> None: - # if not player.state == PlayerState.ON_DESTINATION: - # raise PurchaseForbiddenForPlayer() - # if self._purchase_countdown_timer: - # self._purchase_countdown_timer.stop() - # await self.ws_server.send_product_purchase_done_message( - # player=player, product=product - # ) - # await asyncio.sleep(settings.game.PURCHASE_FINISHED_DELAY) - # await self.reset_game() + async def product_selection_done(self) -> None: + logging.info("Product selection done, unlocking game") + if self._purchase_countdown_timer: + self._purchase_countdown_timer.stop() + await self.ws_server.send_product_selection_done_message() + await self.reset_game() def _reset_player(self, player) -> None: # move player to start position diff --git a/hopper/models/config.py b/hopper/models/config.py index 0af7fae..17aa091 100644 --- a/hopper/models/config.py +++ b/hopper/models/config.py @@ -10,7 +10,6 @@ from hopper.models.product import Product class GameSettings: MOVE_DELAY: float = 0.5 # seconds PURCHASE_START_DELAY: float = 2 # seconds - PURCHASE_FINISHED_DELAY: float = 2 # seconds @dataclass diff --git a/hopper/models/ws_dto.py b/hopper/models/ws_dto.py index bf7765a..53e24bd 100644 --- a/hopper/models/ws_dto.py +++ b/hopper/models/ws_dto.py @@ -33,11 +33,6 @@ class GameDumpDto(BaseModel): layers: list[LayerDto] -class ProductPurchaseTimerDto(BaseModel): - time_left: int - player: PlayerDto - - class PlayerReachedDestinationDto(BaseModel): player: PlayerDto @@ -55,19 +50,19 @@ class WSMessage(GenericModel): def to_str(self) -> str: return json.dumps(self.dict()) + @classmethod + @property + def message_type(cls) -> str: + return cls.__fields__["message"].default + class WSGameDumpMessage(WSMessage): message: str = "game_dump" data: GameDumpDto -class WSProductPurchaseTimerTickMessage(WSMessage): - message: str = "product_purchase_timer_tick" - data: ProductPurchaseTimerDto - - -class WSProductPurchaseDoneMessage(WSMessage): - message: str = "product_purchase_done" +class WSProductSelectionDoneMessage(WSMessage): + message: str = "product_selection_done" class WSPlayerReachedDestinationMessage(WSMessage): diff --git a/hopper/ws_server.py b/hopper/ws_server.py index 6c0eed2..aab7914 100644 --- a/hopper/ws_server.py +++ b/hopper/ws_server.py @@ -1,22 +1,20 @@ import asyncio +import json import logging from threading import Thread -from typing import Iterable, Optional import websockets from websockets import WebSocketServerProtocol from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK from hopper.models.player import Player -from hopper.models.product import Product from hopper.models.ws_dto import ( GameDumpDto, PlayerReachedDestinationDto, - ProductPurchaseTimerDto, WSGameDumpMessage, WSMessage, WSPlayerReachedDestinationMessage, - WSProductPurchaseTimerTickMessage, + WSProductSelectionDoneMessage, ) @@ -26,6 +24,31 @@ class WSServer(Thread): self.port = port super().__init__(daemon=True) + async def handle_rcv_message( + self, client: WebSocketServerProtocol, raw_message: str + ) -> None: + try: + ws_message = json.loads(raw_message) + except Exception as ex: + logging.error( + f"Error decoding WS message from {client.id} {raw_message}: {ex}" + ) + return None + + data_message = ws_message.get("message") + if data_message == WSProductSelectionDoneMessage.message_type: + await self.handle_rcv_product_selection_done(client) + + async def handle_rcv_product_selection_done( + self, client: WebSocketServerProtocol + ) -> None: + logging.info(f"Handle WSProductSelectionDoneMessage: {client.id}") + # avoid circular imports + from hopper.api.dependencies import get_game_engine + + engine = get_game_engine() + await engine.product_selection_done() + async def handler(self, websocket: WebSocketServerProtocol) -> None: """New handler instance spawns for each connected client""" self.connected_clients.add(websocket) @@ -39,7 +62,8 @@ class WSServer(Thread): while connected: try: # we're expecting nothing from client, but read if client sends a message - await websocket.recv() + rcv_data = await websocket.recv() + await self.handle_rcv_message(client=websocket, raw_message=rcv_data) except ConnectionClosedOK: logging.info(f"Connection closed OK for client: {websocket.id}") connected = False @@ -98,26 +122,10 @@ class WSServer(Thread): ) await self.send_message_to_clients(message) - async def send_product_purchase_time_left_message( - self, player: Player, time_left: int - ) -> None: - message = WSProductPurchaseTimerTickMessage( - data=ProductPurchaseTimerDto( - player=player, - time_left=time_left, - ) - ) + async def send_product_selection_done_message(self) -> None: + message = WSProductSelectionDoneMessage() await self.send_message_to_clients(message) - async def send_product_purchase_done_message( - self, player: Player, product: Optional[Product] = None - ) -> None: - # message = WSProductPurchaseDoneMessage( - # data=ProductPurchaseDoneDto(player=player, product=product), - # ) - # await self.send_message_to_clients(message) - ... - async def run_async(self) -> None: logging.info( f"Starting FairHopper Websockets Server on {self.host}:{self.port}"