Product selection message handler

This commit is contained in:
Eden Kirin
2023-05-11 15:08:24 +02:00
parent 69e087c0c9
commit 9151aa3e1e
8 changed files with 113 additions and 89 deletions

View File

@ -170,11 +170,11 @@ package Masterpiece #seashell {
usecase Game as "Game Engine" usecase Game as "Game Engine"
usecase WS as "WS Server" usecase WS as "WS Server"
} }
usecase Vis as "Visualisation\nService" usecase Vis as "Flutter\nVisualisation\nService"
} }
usecase ExtVis1 as "Visualisation\nService" usecase ExtVis1 as "Visualisation\nClient"
usecase ExtVis2 as "Visualisation\nService" usecase ExtVis2 as "Visualisation\nClient"
P1 -left-> API: REST API P1 -left-> API: REST API
P2 -left-> API: REST API P2 -left-> API: REST API
@ -204,41 +204,58 @@ Game ->o WS: Send initial state
Client1 ->o WS: Client connect Client1 ->o WS: Client connect
activate WS #coral activate WS #coral
WS -> Client1: Game state WS -> Client1: Game state
deactivate deactivate WS
Client2 ->o WS: Client connect Client2 ->o WS: Client connect
activate WS #coral activate WS #coral
WS -> Client2: Game state WS -> Client2: Game state
deactivate deactivate WS
loop #lightyellow On game state change loop #lightyellow On game state change
Game ->o WS: Game state Game ->o WS: Game state
activate WS #coral activate WS #coral
WS o-> Client1: Game state WS o-> Client1: Game state
WS o-> Client2: Game state WS o-> Client2: Game state
deactivate deactivate WS
end end
== Product purchase mode == == Player reached destination ==
Game -> WS: Purchase start Game -> Game: Lock game for other players
activate Game
Game -> WS: Player reached destination
activate WS #coral activate WS #coral
WS o-> Client1: Purchase start WS o-> Client1: Select product
WS o-> Client2: Purchase start WS o-> Client2: Select product
deactivate deactivate WS
deactivate Game
loop #lightyellow Purchase countdown timer loop #lightyellow Product select countdown timer (60s)
Game ->o WS: Timer count down Game ->o WS: Timer timeout
activate Game
activate WS #coral activate WS #coral
WS o-> Client1: Purchase time left WS o-> Client1: Cancel selection
WS o-> Client2: Purchase time left WS o-> Client2: Cancel selection
deactivate deactivate WS
Game -> Game: Unlock game
deactivate Game
end end
Game -> WS: Purchase done
Client1 -> WS: Product selected
activate WS #coral activate WS #coral
WS o-> Client1: Purchase done WS o-> Game: Product selected
WS o-> Client2: Purchase done activate Game
deactivate 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
``` ```

View File

@ -8,7 +8,9 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"
integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V"
crossorigin="anonymous"></script>
<script src="js/config.js"></script> <script src="js/config.js"></script>
<script src="js/frontend.js"></script> <script src="js/frontend.js"></script>
@ -59,7 +61,7 @@
moves. moves.
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary"> <button type="button" class="btn btn-primary" id="finish-product-selection" data-bs-dismiss="modal">
Finish product selection Finish product selection
</button> </button>
</div> </div>

View File

@ -2,6 +2,8 @@ if (typeof FAIRHOPPER_WS_SERVER === "undefined") {
var FAIRHOPPER_WS_SERVER = "ws://127.0.0.1:8011"; var FAIRHOPPER_WS_SERVER = "ws://127.0.0.1:8011";
} }
let ws = null;
const BOARD_ICONS = { const BOARD_ICONS = {
PLAYER: "😀", PLAYER: "😀",
PLAYER_ON_DESTINATION: "😎", PLAYER_ON_DESTINATION: "😎",
@ -146,7 +148,7 @@ function productPurchaseDone(product) {
function wsConnect() { function wsConnect() {
console.log("Attempting to connect to", FAIRHOPPER_WS_SERVER); console.log("Attempting to connect to", FAIRHOPPER_WS_SERVER);
let ws = new WebSocket(FAIRHOPPER_WS_SERVER); ws = new WebSocket(FAIRHOPPER_WS_SERVER);
ws.onopen = () => { ws.onopen = () => {
console.log("WS connected"); console.log("WS connected");
@ -178,6 +180,7 @@ function wsConnect() {
}; };
ws.onclose = (e) => { ws.onclose = (e) => {
ws = null;
setTimeout(() => { setTimeout(() => {
wsConnect(); wsConnect();
}, 1000); }, 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 = () => { window.onload = () => {
document.getElementById("finish-product-selection").onclick = () => {
finishProductSelection();
};
wsConnect(); wsConnect();
}; };

View File

@ -5,11 +5,7 @@ from typing import Optional
from hopper.countdown_timer import CountdownTimer from hopper.countdown_timer import CountdownTimer
from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState
from hopper.errors import ( from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds
Collision,
GameLockForMovement,
PositionOutOfBounds,
)
from hopper.models.board import ( from hopper.models.board import (
BOARD_DUMP_CHARS, BOARD_DUMP_CHARS,
BoardLayout, BoardLayout,
@ -179,24 +175,17 @@ class GameEngine:
await self.ws_server.send_player_reached_destination_message(player=player) await self.ws_server.send_player_reached_destination_message(player=player)
logging.info( 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: def on_purchase_timer_tick(time_left) -> None:
logging.info(f"Purchase countdown timer tick, time left: {time_left}") logging.info(f"Product selection countdown timer tick, time left: {time_left}")
asyncio.run(
self.ws_server.send_product_purchase_time_left_message(
player=player, time_left=time_left
)
)
def on_purchase_timer_done() -> None: 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 self._purchase_countdown_timer = None
asyncio.run( asyncio.run(
self.ws_server.send_product_purchase_done_message( self.ws_server.send_product_selection_done_message()
player=player, product=None
)
) )
self.game_state = GameState.RUNNING self.game_state = GameState.RUNNING
asyncio.run(self.send_game_dump()) asyncio.run(self.send_game_dump())
@ -210,16 +199,12 @@ class GameEngine:
await asyncio.sleep(settings.game.PURCHASE_START_DELAY) await asyncio.sleep(settings.game.PURCHASE_START_DELAY)
# async def purchase_product(self, player: Player, product: Product) -> None: async def product_selection_done(self) -> None:
# if not player.state == PlayerState.ON_DESTINATION: logging.info("Product selection done, unlocking game")
# raise PurchaseForbiddenForPlayer() if self._purchase_countdown_timer:
# if self._purchase_countdown_timer: self._purchase_countdown_timer.stop()
# self._purchase_countdown_timer.stop() await self.ws_server.send_product_selection_done_message()
# await self.ws_server.send_product_purchase_done_message( await self.reset_game()
# player=player, product=product
# )
# await asyncio.sleep(settings.game.PURCHASE_FINISHED_DELAY)
# await self.reset_game()
def _reset_player(self, player) -> None: def _reset_player(self, player) -> None:
# move player to start position # move player to start position

View File

@ -10,7 +10,6 @@ from hopper.models.product import Product
class GameSettings: class GameSettings:
MOVE_DELAY: float = 0.5 # seconds MOVE_DELAY: float = 0.5 # seconds
PURCHASE_START_DELAY: float = 2 # seconds PURCHASE_START_DELAY: float = 2 # seconds
PURCHASE_FINISHED_DELAY: float = 2 # seconds
@dataclass @dataclass

View File

@ -33,11 +33,6 @@ class GameDumpDto(BaseModel):
layers: list[LayerDto] layers: list[LayerDto]
class ProductPurchaseTimerDto(BaseModel):
time_left: int
player: PlayerDto
class PlayerReachedDestinationDto(BaseModel): class PlayerReachedDestinationDto(BaseModel):
player: PlayerDto player: PlayerDto
@ -55,19 +50,19 @@ class WSMessage(GenericModel):
def to_str(self) -> str: def to_str(self) -> str:
return json.dumps(self.dict()) return json.dumps(self.dict())
@classmethod
@property
def message_type(cls) -> str:
return cls.__fields__["message"].default
class WSGameDumpMessage(WSMessage): class WSGameDumpMessage(WSMessage):
message: str = "game_dump" message: str = "game_dump"
data: GameDumpDto data: GameDumpDto
class WSProductPurchaseTimerTickMessage(WSMessage): class WSProductSelectionDoneMessage(WSMessage):
message: str = "product_purchase_timer_tick" message: str = "product_selection_done"
data: ProductPurchaseTimerDto
class WSProductPurchaseDoneMessage(WSMessage):
message: str = "product_purchase_done"
class WSPlayerReachedDestinationMessage(WSMessage): class WSPlayerReachedDestinationMessage(WSMessage):

View File

@ -1,22 +1,20 @@
import asyncio import asyncio
import json
import logging import logging
from threading import Thread from threading import Thread
from typing import Iterable, Optional
import websockets import websockets
from websockets import WebSocketServerProtocol from websockets import WebSocketServerProtocol
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
from hopper.models.player import Player from hopper.models.player import Player
from hopper.models.product import Product
from hopper.models.ws_dto import ( from hopper.models.ws_dto import (
GameDumpDto, GameDumpDto,
PlayerReachedDestinationDto, PlayerReachedDestinationDto,
ProductPurchaseTimerDto,
WSGameDumpMessage, WSGameDumpMessage,
WSMessage, WSMessage,
WSPlayerReachedDestinationMessage, WSPlayerReachedDestinationMessage,
WSProductPurchaseTimerTickMessage, WSProductSelectionDoneMessage,
) )
@ -26,6 +24,31 @@ class WSServer(Thread):
self.port = port self.port = port
super().__init__(daemon=True) 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: async def handler(self, websocket: WebSocketServerProtocol) -> None:
"""New handler instance spawns for each connected client""" """New handler instance spawns for each connected client"""
self.connected_clients.add(websocket) self.connected_clients.add(websocket)
@ -39,7 +62,8 @@ class WSServer(Thread):
while connected: while connected:
try: try:
# we're expecting nothing from client, but read if client sends a message # 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: except ConnectionClosedOK:
logging.info(f"Connection closed OK for client: {websocket.id}") logging.info(f"Connection closed OK for client: {websocket.id}")
connected = False connected = False
@ -98,26 +122,10 @@ class WSServer(Thread):
) )
await self.send_message_to_clients(message) await self.send_message_to_clients(message)
async def send_product_purchase_time_left_message( async def send_product_selection_done_message(self) -> None:
self, player: Player, time_left: int message = WSProductSelectionDoneMessage()
) -> None:
message = WSProductPurchaseTimerTickMessage(
data=ProductPurchaseTimerDto(
player=player,
time_left=time_left,
)
)
await self.send_message_to_clients(message) 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: async def run_async(self) -> None:
logging.info( logging.info(
f"Starting FairHopper Websockets Server on {self.host}:{self.port}" f"Starting FairHopper Websockets Server on {self.host}:{self.port}"