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
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}"