From 69e087c0c9b944820a4b0db92ba0bf7bbab38924 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Wed, 10 May 2023 15:49:08 +0200 Subject: [PATCH 1/4] Drop old purchase views and models --- frontend/index.html | 38 +++++++++++++++++++++++++- frontend/js/frontend.js | 14 ++++++++++ hopper/api/dto.py | 15 ----------- hopper/api/views.py | 60 ----------------------------------------- hopper/engine.py | 26 ++++++++---------- hopper/errors.py | 4 --- hopper/models/ws_dto.py | 21 +++++---------- hopper/ws_server.py | 28 ++++++++----------- 8 files changed, 79 insertions(+), 127 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 1f06bca..25ea775 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,8 +6,9 @@ + integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> + @@ -19,6 +20,9 @@

FairHopper Visualisation Client

+

@@ -42,6 +46,38 @@

+ + + + + \ No newline at end of file diff --git a/frontend/js/frontend.js b/frontend/js/frontend.js index c9d4524..9caaa62 100644 --- a/frontend/js/frontend.js +++ b/frontend/js/frontend.js @@ -96,6 +96,17 @@ function renderGameDump(data) { renderPlayers(data.players); } +function playerReachedDestination(data) { + console.log(data); + + const dlgElement = document.getElementById("player-on-destination-modal"); + dlgElement.querySelector(".player-name").textContent = data.player.name; + dlgElement.querySelector(".move-count").textContent = data.player.move_count; + + const modal = new bootstrap.Modal(dlgElement); + modal.show(); +} + function productPurchaseStart(products, purchaseTimeout) { console.log("productPurchaseStart:", products); const containerElement = document.getElementById("purchase-container"); @@ -149,6 +160,9 @@ function wsConnect() { case "game_dump": renderGameDump(wsMessage.data); break; + case "player_on_destination": + playerReachedDestination(wsMessage.data); + break; case "product_purchase_start": productPurchaseStart(wsMessage.data.products, wsMessage.data.timeout); break; diff --git a/hopper/api/dto.py b/hopper/api/dto.py index 5654929..8a923d9 100644 --- a/hopper/api/dto.py +++ b/hopper/api/dto.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from pydantic import BaseModel as PydanticBaseModel from hopper.enums import PlayerState @@ -40,11 +38,6 @@ class DestinationDto(BaseModel): position: PositionDto -class ProductDto(BaseModel): - name: str - id: str - description: Optional[str] = None - class StartGameRequestDto(BaseModel): player_name: str @@ -68,11 +61,3 @@ class PlayerInfoResponseDto(MovePlayerResponseDto): class ErrorResponseDto(BaseModel): detail: str - - -class GetProductsResponse(BaseModel): - products: list[ProductDto] - - -class PurchaseProductDto(BaseModel): - product_id: str diff --git a/hopper/api/views.py b/hopper/api/views.py index 5102edb..fdb0038 100644 --- a/hopper/api/views.py +++ b/hopper/api/views.py @@ -6,12 +6,9 @@ from hopper.api.dto import ( DestinationDto, ErrorResponseDto, GameInfoDto, - GetProductsResponse, MovePlayerResponseDto, PingResponse, PlayerInfoResponseDto, - ProductDto, - PurchaseProductDto, StartGameRequestDto, StartGameResponseDto, ) @@ -21,10 +18,8 @@ from hopper.errors import ( Collision, GameLockForMovement, PositionOutOfBounds, - PurchaseForbiddenForPlayer, ) from hopper.models.player import Player -from settings import settings router = APIRouter() @@ -153,58 +148,3 @@ async def move_player( response.status_code = status.HTTP_200_OK return MovePlayerResponseDto(player=player) - - -@router.get("/products", response_model=GetProductsResponse) -async def get_products() -> GetProductsResponse: - return GetProductsResponse( - products=settings.products, - ) - - -@router.get("/products/{id}", response_model=ProductDto) -async def get_product(id: str) -> ProductDto: - for product in settings.products: - if product.id == id: - return ProductDto.from_orm(product) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Product not found" - ) - - -@router.post( - "/player/{id}/product/purchase", - response_model=ProductDto, - responses={ - status.HTTP_200_OK: { - "model": ProductDto, - "description": "Product purchased", - }, - status.HTTP_403_FORBIDDEN: { - "model": ErrorResponseDto, - "description": "Purchase forbidden for this player", - }, - status.HTTP_404_NOT_FOUND: { - "model": ErrorResponseDto, - "description": " Player with id not found, probably kicked out", - }, - }, -) -async def purchase_product( - body: PurchaseProductDto, - engine: GameEngine = Depends(get_game_engine), - player: Player = Depends(get_player), -) -> ProductDto: - for product in settings.products: - if product.id == body.product_id: - try: - await engine.purchase_product(player=player, product=product) - return ProductDto.from_orm(product) - except PurchaseForbiddenForPlayer: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Purchase forbidden for this player", - ) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Product not found" - ) diff --git a/hopper/engine.py b/hopper/engine.py index 9493da8..9fce5a4 100644 --- a/hopper/engine.py +++ b/hopper/engine.py @@ -9,7 +9,6 @@ from hopper.errors import ( Collision, GameLockForMovement, PositionOutOfBounds, - PurchaseForbiddenForPlayer, ) from hopper.models.board import ( BOARD_DUMP_CHARS, @@ -22,7 +21,6 @@ from hopper.models.board import ( create_random_position, ) from hopper.models.player import Player, PlayerList, Position -from hopper.models.product import Product from hopper.watchdog import InactivityWatchdog from hopper.ws_server import WSServer from settings import settings @@ -178,9 +176,7 @@ class GameEngine: self.game_state = GameState.LOCK_FOR_MOVEMENT await self.send_game_dump() - await self.ws_server.send_product_purchase_start_message( - player=player, products=settings.products - ) + await self.ws_server.send_player_reached_destination_message(player=player) logging.info( f"Starting purchase countdown timer for {settings.purchase_timeout} seconds" @@ -214,16 +210,16 @@ 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 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() def _reset_player(self, player) -> None: # move player to start position diff --git a/hopper/errors.py b/hopper/errors.py index f4159be..cf74c54 100644 --- a/hopper/errors.py +++ b/hopper/errors.py @@ -12,7 +12,3 @@ class Collision(BaseError): class GameLockForMovement(BaseError): ... - - -class PurchaseForbiddenForPlayer(BaseError): - ... diff --git a/hopper/models/ws_dto.py b/hopper/models/ws_dto.py index d047b97..bf7765a 100644 --- a/hopper/models/ws_dto.py +++ b/hopper/models/ws_dto.py @@ -12,7 +12,6 @@ from hopper.api.dto import ( DestinationDto, PlayerDto, PositionDto, - ProductDto, ) from hopper.enums import ObjectType @@ -34,20 +33,13 @@ class GameDumpDto(BaseModel): layers: list[LayerDto] -class ProductPurchaseStartDto(BaseModel): - player: PlayerDto - products: list[ProductDto] - timeout: int - - class ProductPurchaseTimerDto(BaseModel): time_left: int player: PlayerDto -class ProductPurchaseDoneDto(BaseModel): +class PlayerReachedDestinationDto(BaseModel): player: PlayerDto - product: Optional[ProductDto] = None TMessageData = TypeVar("TMessageData", bound=BaseModel) @@ -69,11 +61,6 @@ class WSGameDumpMessage(WSMessage): data: GameDumpDto -class WSProductPurchaseStartMessage(WSMessage): - message: str = "product_purchase_start" - data: ProductPurchaseStartDto - - class WSProductPurchaseTimerTickMessage(WSMessage): message: str = "product_purchase_timer_tick" data: ProductPurchaseTimerDto @@ -81,4 +68,8 @@ class WSProductPurchaseTimerTickMessage(WSMessage): class WSProductPurchaseDoneMessage(WSMessage): message: str = "product_purchase_done" - data: ProductPurchaseDoneDto + + +class WSPlayerReachedDestinationMessage(WSMessage): + message: str = "player_reached_destination" + data: PlayerReachedDestinationDto diff --git a/hopper/ws_server.py b/hopper/ws_server.py index 9b6ca2c..6c0eed2 100644 --- a/hopper/ws_server.py +++ b/hopper/ws_server.py @@ -5,22 +5,19 @@ from typing import Iterable, Optional import websockets from websockets import WebSocketServerProtocol -from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError +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, - ProductPurchaseDoneDto, - ProductPurchaseStartDto, + PlayerReachedDestinationDto, ProductPurchaseTimerDto, WSGameDumpMessage, WSMessage, - WSProductPurchaseDoneMessage, - WSProductPurchaseStartMessage, + WSPlayerReachedDestinationMessage, WSProductPurchaseTimerTickMessage, ) -from settings import settings class WSServer(Thread): @@ -93,14 +90,10 @@ class WSServer(Thread): message = self._create_game_dump_message() await self.send_message_to_clients(message) - async def send_product_purchase_start_message( - self, player: Player, products: Iterable[Product] - ) -> None: - message = WSProductPurchaseStartMessage( - data=ProductPurchaseStartDto( + async def send_player_reached_destination_message(self, player: Player) -> None: + message = WSPlayerReachedDestinationMessage( + data=PlayerReachedDestinationDto( player=player, - products=products, - timeout=settings.purchase_timeout, ) ) await self.send_message_to_clients(message) @@ -119,10 +112,11 @@ class WSServer(Thread): 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) + # message = WSProductPurchaseDoneMessage( + # data=ProductPurchaseDoneDto(player=player, product=product), + # ) + # await self.send_message_to_clients(message) + ... async def run_async(self) -> None: logging.info( From 9151aa3e1ef85246e5c7054bb63c87edf751eb43 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Thu, 11 May 2023 15:08:24 +0200 Subject: [PATCH 2/4] 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}" From 76ee207bce3c975228acd05247ed6042da542548 Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Thu, 11 May 2023 16:09:32 +0200 Subject: [PATCH 3/4] Frontend support for product selection --- frontend/js/frontend.js | 61 +++++++---------------------------------- 1 file changed, 10 insertions(+), 51 deletions(-) diff --git a/frontend/js/frontend.js b/frontend/js/frontend.js index 3d38c4e..27d3a4a 100644 --- a/frontend/js/frontend.js +++ b/frontend/js/frontend.js @@ -3,6 +3,7 @@ if (typeof FAIRHOPPER_WS_SERVER === "undefined") { } let ws = null; +let playerOnDestinationModal = null; const BOARD_ICONS = { PLAYER: "๐Ÿ˜€", @@ -89,8 +90,6 @@ function renderDestination(position) { } function renderGameDump(data) { - closePurchaseWindow(); - createBoard(data.board); renderObstacles(data.layers); renderDestination(data.destination.position); @@ -99,51 +98,14 @@ function renderGameDump(data) { } function playerReachedDestination(data) { - console.log(data); - const dlgElement = document.getElementById("player-on-destination-modal"); dlgElement.querySelector(".player-name").textContent = data.player.name; dlgElement.querySelector(".move-count").textContent = data.player.move_count; - - const modal = new bootstrap.Modal(dlgElement); - modal.show(); + playerOnDestinationModal.show(); } -function productPurchaseStart(products, purchaseTimeout) { - console.log("productPurchaseStart:", products); - const containerElement = document.getElementById("purchase-container"); - const contentElement = document.getElementById("products-content"); - const purchaseTimeoutElement = document.getElementById("purchase-countdown"); - - contentElement.innerHTML = products - .map((product) => { - return ` -
- ${product.name} -
-
${product.name}
-
-
- `; - }) - .join(""); - containerElement.classList.remove("d-none"); - purchaseTimeoutElement.innerText = purchaseTimeout; -} - -function productPurchaseTimerTick(timeLeft) { - const purchaseTimeoutElement = document.getElementById("purchase-countdown"); - purchaseTimeoutElement.innerText = timeLeft; -} - -function closePurchaseWindow() { - const container = document.getElementById("purchase-container"); - container.classList.add("d-none"); -} - -function productPurchaseDone(product) { - const cardContainer = document.getElementById(`product-${product.id}`); - cardContainer.classList.add("selected"); +function productSelectionDone() { + playerOnDestinationModal.hide(); } function wsConnect() { @@ -162,17 +124,11 @@ function wsConnect() { case "game_dump": renderGameDump(wsMessage.data); break; - case "player_on_destination": + case "player_reached_destination": playerReachedDestination(wsMessage.data); break; - case "product_purchase_start": - productPurchaseStart(wsMessage.data.products, wsMessage.data.timeout); - break; - case "product_purchase_timer_tick": - productPurchaseTimerTick(wsMessage.data.time_left); - break; - case "product_purchase_done": - productPurchaseDone(wsMessage.data.product); + case "product_selection_done": + productSelectionDone(); break; default: console.error("Unknown message:", wsMessage); @@ -204,6 +160,9 @@ function finishProductSelection() { } window.onload = () => { + const dlgElement = document.getElementById("player-on-destination-modal"); + playerOnDestinationModal = new bootstrap.Modal(dlgElement); + document.getElementById("finish-product-selection").onclick = () => { finishProductSelection(); }; From d660845d3067fa6e46b7393d87a16631cffb29be Mon Sep 17 00:00:00 2001 From: Eden Kirin Date: Thu, 11 May 2023 19:36:16 +0200 Subject: [PATCH 4/4] Endgame WS messages & docs --- README.md | 117 +++++++++------------------------------- frontend/index.html | 15 ------ frontend/js/frontend.js | 7 +++ hopper/engine.py | 2 +- hopper/models/ws_dto.py | 12 ++--- hopper/ws_server.py | 9 +++- 6 files changed, 47 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index b428d79..3cf1cd2 100644 --- a/README.md +++ b/README.md @@ -234,8 +234,8 @@ loop #lightyellow Product select countdown timer (60s) Game ->o WS: Timer timeout activate Game activate WS #coral - WS o-> Client1: Cancel selection - WS o-> Client2: Cancel selection + WS o-> Client1: Selection timeout + WS o-> Client2: Selection timeout deactivate WS Game -> Game: Unlock game deactivate Game @@ -400,6 +400,8 @@ Response body: ### Game state structure +Direction: Game server -> Clients + Message: `game_dump` Data: @@ -503,109 +505,42 @@ Data: } ``` -### Product purchase start +### Player reached destination -Message: `product_purchase_start` +Direction: Game server -> Clients + +Message: `player_reached_destination` Data: ```json { "player": { - "id": "test-player-pero", - "name": "Pero", + "id": "2e0f1a50-eaa6-4efd-b0c3-adbf7000eec2", + "name": "Joso", "active": true, "position": { - "x": 10, - "y": 10 + "x": 5, + "y": 5 }, - "move_count": 1, - "move_attempt_count": 1, - "state": "ON_DESTINATION" - }, - "products": [ - { - "name": "CocaCola", - "id": "cocacola-id", - "description": null - }, - { - "name": "Pepsi", - "id": "pepsi-id", - "description": null - }, - { - "name": "Fanta", - "id": "fanta-id", - "description": null - }, - { - "name": "Snickers", - "id": "snickers-id", - "description": null - }, - { - "name": "Mars", - "id": "mars-id", - "description": null - }, - { - "name": "Burek", - "id": "burek-id", - "description": null - } - ], - "timeout": 5 -} -``` - -### Product purchase timer tick - -Message: `product_purchase_timer_tick` - -Data: -```json -{ - "time_left": 4, - "player": { - "id": "test-player-pero", - "name": "Pero", - "active": true, - "position": { - "x": 10, - "y": 10 - }, - "move_count": 1, - "move_attempt_count": 1, + "move_count": 6, + "move_attempt_count": 6, "state": "ON_DESTINATION" } } ``` -### Product purchase timer done +### Product selection timeout -Message: `product_purchase_done` +Direction: Game server -> Clients -Data: -```json -{ - "player": { - "id": "test-player-pero", - "name": "Pero", - "active": true, - "position": { - "x": 10, - "y": 10 - }, - "move_count": 1, - "move_attempt_count": 1, - "state": "ON_DESTINATION" - }, - "product": { - "name": "CocaCola", - "id": "cocacola-id", - "description": null - } -} -``` +Message: `product_selection_timeout` -If product selection timeout occured, product will be null. +Data: `null` + +### Product selection done + +Message: `product_selection_done` + +Direction: Client -> Game server, Game server -> Clients + +Data: `null` diff --git a/frontend/index.html b/frontend/index.html index c61962f..c836545 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -22,9 +22,6 @@

FairHopper Visualisation Client

-

@@ -68,18 +65,6 @@

- - - \ No newline at end of file diff --git a/frontend/js/frontend.js b/frontend/js/frontend.js index 27d3a4a..c560940 100644 --- a/frontend/js/frontend.js +++ b/frontend/js/frontend.js @@ -104,6 +104,10 @@ function playerReachedDestination(data) { playerOnDestinationModal.show(); } +function productSelectionTimeout() { + playerOnDestinationModal.hide(); +} + function productSelectionDone() { playerOnDestinationModal.hide(); } @@ -127,6 +131,9 @@ function wsConnect() { case "player_reached_destination": playerReachedDestination(wsMessage.data); break; + case "product_selection_timeout": + productSelectionTimeout(); + break; case "product_selection_done": productSelectionDone(); break; diff --git a/hopper/engine.py b/hopper/engine.py index 201b1b8..5646d61 100644 --- a/hopper/engine.py +++ b/hopper/engine.py @@ -185,7 +185,7 @@ class GameEngine: logging.info("Ding ding! Product selection countdown timer timeout") self._purchase_countdown_timer = None asyncio.run( - self.ws_server.send_product_selection_done_message() + self.ws_server.send_product_selection_timeout_message() ) self.game_state = GameState.RUNNING asyncio.run(self.send_game_dump()) diff --git a/hopper/models/ws_dto.py b/hopper/models/ws_dto.py index 53e24bd..d5ed16c 100644 --- a/hopper/models/ws_dto.py +++ b/hopper/models/ws_dto.py @@ -6,13 +6,7 @@ from typing import Optional, TypeVar from pydantic import Field from pydantic.generics import GenericModel -from hopper.api.dto import ( - BaseModel, - BoardDto, - DestinationDto, - PlayerDto, - PositionDto, -) +from hopper.api.dto import BaseModel, BoardDto, DestinationDto, PlayerDto, PositionDto from hopper.enums import ObjectType @@ -65,6 +59,10 @@ class WSProductSelectionDoneMessage(WSMessage): message: str = "product_selection_done" +class WSProductSelectionTimeoutMessage(WSMessage): + message: str = "product_selection_timeout" + + class WSPlayerReachedDestinationMessage(WSMessage): message: str = "player_reached_destination" data: PlayerReachedDestinationDto diff --git a/hopper/ws_server.py b/hopper/ws_server.py index aab7914..6ee8809 100644 --- a/hopper/ws_server.py +++ b/hopper/ws_server.py @@ -15,6 +15,7 @@ from hopper.models.ws_dto import ( WSMessage, WSPlayerReachedDestinationMessage, WSProductSelectionDoneMessage, + WSProductSelectionTimeoutMessage, ) @@ -63,7 +64,9 @@ class WSServer(Thread): try: # we're expecting nothing from client, but read if client sends a message rcv_data = await websocket.recv() - await self.handle_rcv_message(client=websocket, raw_message=rcv_data) + 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 @@ -126,6 +129,10 @@ class WSServer(Thread): message = WSProductSelectionDoneMessage() await self.send_message_to_clients(message) + async def send_product_selection_timeout_message(self) -> None: + message = WSProductSelectionTimeoutMessage() + 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}"