Send player info with product purchase data

This commit is contained in:
Eden Kirin
2023-03-31 11:51:05 +02:00
parent 210a6aff7c
commit 659ca82d74
9 changed files with 141 additions and 98 deletions

View File

@ -100,6 +100,7 @@ WebSockets server runs on port **8011**. To run WS Server on different port, edi
### Architecture ### Architecture
```plantuml ```plantuml
scale 1024 width
actor "Player 1" as P1 actor "Player 1" as P1
actor "Player 2" as P2 actor "Player 2" as P2
actor "Player 3" as P3 actor "Player 3" as P3
@ -129,6 +130,7 @@ WS --> ExtVis2: WS Game State
### WebSockets ### WebSockets
```plantuml ```plantuml
scale 1024 width
box "FairHopper Game Server" #lightcyan box "FairHopper Game Server" #lightcyan
participant Game as "Game Engine" participant Game as "Game Engine"
participant WS as "WS Server" participant WS as "WS Server"
@ -136,6 +138,8 @@ endbox
participant Client1 as "Visualisation\nClient 1" participant Client1 as "Visualisation\nClient 1"
participant Client2 as "Visualisation\nClient 2" participant Client2 as "Visualisation\nClient 2"
== Player movement mode ==
Game ->o WS: Send initial state Game ->o WS: Send initial state
Client1 ->o WS: Client connect Client1 ->o WS: Client connect
@ -155,6 +159,27 @@ loop #lightyellow On game state change
WS o-> Client2: Game state WS o-> Client2: Game state
deactivate deactivate
end end
== Product purchase mode ==
Game -> WS: Purchase start
activate WS #coral
WS o-> Client1: Purchase start
WS o-> Client2: Purchase start
deactivate
loop #lightyellow Purchase countdown timer
Game ->o WS: Timer count down
activate WS #coral
WS o-> Client1: Purchase time left
WS o-> Client2: Purchase time left
deactivate
end
Game -> WS: Purchase done
activate WS #coral
WS o-> Client1: Purchase done
WS o-> Client2: Purchase done
deactivate
``` ```

View File

@ -18,7 +18,12 @@
FairHopper Visualisation Client FairHopper Visualisation Client
</h1> </h1>
<div id="purchase-container" class="purchase-container d-none"> <div id="purchase-container" class="purchase-container d-none">
<h3>Product selection</h3> <div class="d-flex header">
<h3>
Product selection
</h3>
<h3 id="purchase-countdown" class="countdown"></h3>
</div>
<div id="products-content" class="products-content"></div> <div id="products-content" class="products-content"></div>
</div> </div>
<div class="row"> <div class="row">
@ -127,10 +132,11 @@
renderPlayers(data.players); renderPlayers(data.players);
} }
function productPurchaseStart(products) { function productPurchaseStart(products, purchaseTimeout) {
console.log("productPurchaseStart:", products); console.log("productPurchaseStart:", products);
const containerElement = document.getElementById("purchase-container"); const containerElement = document.getElementById("purchase-container");
const contentElement = document.getElementById("products-content"); const contentElement = document.getElementById("products-content");
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
const html = products.map(product => { const html = products.map(product => {
return ` return `
@ -145,6 +151,12 @@
contentElement.innerHTML = html; contentElement.innerHTML = html;
containerElement.classList.remove("d-none") containerElement.classList.remove("d-none")
purchaseTimeoutElement.innerText = purchaseTimeout;
}
function productPurchaseTimerTick(timeLeft) {
const purchaseTimeoutElement = document.getElementById("purchase-countdown");
purchaseTimeoutElement.innerText = timeLeft;
} }
function productPurchased(product) { function productPurchased(product) {
@ -172,7 +184,10 @@
renderGameDump(wsMessage.data); renderGameDump(wsMessage.data);
break; break;
case "product_purchase_start": case "product_purchase_start":
productPurchaseStart(wsMessage.data.products) productPurchaseStart(wsMessage.data.products, wsMessage.data.timeout)
break;
case "product_purchase_timer_tick":
productPurchaseTimerTick(wsMessage.data.time_left)
break; break;
case "product_purchased": case "product_purchased":
productPurchased(wsMessage.data) productPurchased(wsMessage.data)
@ -199,42 +214,7 @@
window.onload = () => { window.onload = () => {
wsConnect(); wsConnect();
productPurchaseStart([
{
"name": "CocaCola",
"uuid": "4af72121-c4c5-4a28-b514-2ba577a7f6c5",
"description": null
},
{
"name": "Pepsi",
"uuid": "a14ad558-6ab2-4aa7-9456-f525430c38f8",
"description": null
},
{
"name": "Fanta",
"uuid": "7ea2fe22-c938-4217-91ec-e96c040b077f",
"description": null
},
{
"name": "Snickers",
"uuid": "04d8ad0c-fa80-4342-9449-390b162995fd",
"description": null
},
{
"name": "Mars",
"uuid": "f8674776-dc57-418f-b4ea-29f4ec4fdf35",
"description": null
},
{
"name": "Burek",
"uuid": "329942c8-9a6d-42e5-b859-3df577c5bce7",
"description": null
}
])
} }
</script> </script>
</html> </html>

View File

@ -69,11 +69,15 @@ ul.players {
border-radius: 10px; border-radius: 10px;
} }
.purchase-container h3 { .purchase-container .header {
color: white; color: white;
margin-bottom: 20px; margin-bottom: 20px;
} }
.purchase-container .header .countdown {
margin-left: auto;
}
.purchase-container .products-content { .purchase-container .products-content {
display: grid; display: grid;
grid-gap: 10px; grid-gap: 10px;

View File

@ -5,21 +5,24 @@ from typing import Callable, Optional
class CountdownTimer(Thread): class CountdownTimer(Thread):
def __init__( def __init__(
self, seconds: int, callback: Optional[Callable[[], None]] = None self, seconds: int, timer_tick_callback: Optional[Callable[[int], None]] = None, timer_done_callback: Optional[Callable[[], None]] = None
) -> None: ) -> None:
self.seconds = seconds self.seconds = seconds
self.stop_event = Event() self.stop_event = Event()
self.callback = callback self.timer_tick_callback = timer_tick_callback
self.timer_done_callback = timer_done_callback
super().__init__(daemon=True) super().__init__(daemon=True)
def run(self) -> None: def run(self) -> None:
cnt = self.seconds time_left = self.seconds
while cnt and not self.stop_event.is_set(): while time_left and not self.stop_event.is_set():
cnt -= 1
time.sleep(1) time.sleep(1)
time_left -= 1
if self.timer_tick_callback:
self.timer_tick_callback(time_left)
if cnt == 0 and self.callback: if time_left == 0 and self.timer_done_callback:
self.callback() self.timer_done_callback()
def stop(self) -> None: def stop(self) -> None:
self.stop_event.set() self.stop_event.set()

View File

@ -6,7 +6,6 @@ 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 Collision, GameLockForMovement, PositionOutOfBounds from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds
from hopper.interfaces import SendGameDumpInterface
from hopper.models.board import ( from hopper.models.board import (
BOARD_DUMP_CHARS, BOARD_DUMP_CHARS,
BoardLayout, BoardLayout,
@ -19,13 +18,12 @@ from hopper.models.board import (
) )
from hopper.models.player import Player, PlayerList, Position from hopper.models.player import Player, PlayerList, Position
from hopper.watchdog import InactivityWatchdog from hopper.watchdog import InactivityWatchdog
from hopper.ws_server import WSServer
from settings import settings from settings import settings
class GameEngine: class GameEngine:
def __init__( def __init__(self, board: GameBoard, ws_server: WSServer = None) -> None:
self, board: GameBoard, ws_server: Optional[SendGameDumpInterface] = None
) -> None:
self.board = board self.board = board
self.ws_server = ws_server self.ws_server = ws_server
self.players = PlayerList() self.players = PlayerList()
@ -82,7 +80,12 @@ class GameEngine:
if self.ws_server: if self.ws_server:
await self.ws_server.send_game_dump() await self.ws_server.send_game_dump()
#!!!!!!!!!!!!!!!
await self._player_on_destination(player)
#!!!!!!!!!!!!!!!
await asyncio.sleep(settings.game.MOVE_DELAY) await asyncio.sleep(settings.game.MOVE_DELAY)
return player return player
def _create_player_start_position(self) -> Position: def _create_player_start_position(self) -> Position:
@ -171,23 +174,38 @@ class GameEngine:
await self.ws_server.send_game_dump() await self.ws_server.send_game_dump()
self.__debug_print_board() self.__debug_print_board()
await self.ws_server.send_product_purchase_message(products=settings.products) await self.ws_server.send_product_purchase_start_message(
player=player, products=settings.products
logging.info(f"Starting purchase countdown timer for {settings.purchase_timeout} seconds")
self._purchase_countdown_timer = CountdownTimer(
seconds=settings.purchase_timeout,
callback=self._on_purchase_timeout,
) )
self._purchase_countdown_timer.start()
def _on_purchase_timeout(self) -> None: logging.info(
f"Starting purchase countdown timer for {settings.purchase_timeout} seconds"
)
def on_purchase_timer_tick(time_left) -> None:
asyncio.run(
self.ws_server.send_product_purchase_time_left_message(
player=player, time_left=time_left
)
)
def on_purchase_timer_done() -> None:
logging.info("Ding ding! Purchase countdown timer timeout") logging.info("Ding ding! Purchase 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(product=None)) self.ws_server.send_product_purchase_done_message(
player=player, product=None
)
)
self.game_state = GameState.RUNNING self.game_state = GameState.RUNNING
self._purchase_countdown_timer = CountdownTimer(
seconds=settings.purchase_timeout,
timer_tick_callback=on_purchase_timer_tick,
timer_done_callback=on_purchase_timer_done,
)
self._purchase_countdown_timer.start()
def get_board_layout(self) -> BoardLayout: def get_board_layout(self) -> BoardLayout:
return BoardLayout(board=self.board, players=self.players) return BoardLayout(board=self.board, players=self.players)
@ -198,7 +216,7 @@ class GameEngineFactory:
board_width: int, board_width: int,
board_height: int, board_height: int,
obstacle_count: int = 0, obstacle_count: int = 0,
ws_server: Optional[SendGameDumpInterface] = None, ws_server: WSServer = None,
) -> GameEngine: ) -> GameEngine:
board = GameBoard( board = GameBoard(
width=board_width, width=board_width,
@ -224,7 +242,7 @@ class GameEngineFactory:
@staticmethod @staticmethod
def create_default( def create_default(
ws_server: Optional[SendGameDumpInterface] = None, ws_server: WSServer = None,
) -> GameEngine: ) -> GameEngine:
return GameEngineFactory.create( return GameEngineFactory.create(
board_width=settings.board.WIDTH, board_width=settings.board.WIDTH,

View File

@ -1,16 +0,0 @@
from typing import Iterable, Optional, Protocol
from hopper.models.product import Product
class SendGameDumpInterface(Protocol):
async def send_game_dump(self) -> None:
...
async def send_product_purchase_message(self, products: Iterable[Product]) -> None:
...
async def send_product_purchase_done_message(
self, product: Optional[Product] = None
) -> None:
...

View File

@ -26,22 +26,26 @@ class ProductDto(BaseModel):
description: Optional[str] = None description: Optional[str] = None
class GameDumpPlayerDto(PlayerDto):
...
class GameDumpDto(BaseModel): class GameDumpDto(BaseModel):
board: BoardDto board: BoardDto
destination: DestinationDto destination: DestinationDto
players: list[GameDumpPlayerDto] players: list[PlayerDto]
layers: list[LayerDto] layers: list[LayerDto]
class ProductPurchaseStartDto(BaseModel): class ProductPurchaseStartDto(BaseModel):
player: PlayerDto
products: list[ProductDto] products: list[ProductDto]
timeout: int
class ProductPurchaseTimerDto(BaseModel):
time_left: int
player: PlayerDto
class ProductPurchaseDoneDto(BaseModel): class ProductPurchaseDoneDto(BaseModel):
player: PlayerDto
product: Optional[ProductDto] = None product: Optional[ProductDto] = None
@ -64,11 +68,16 @@ class WSGameDumpMessage(WSMessage):
data: GameDumpDto data: GameDumpDto
class WSProductPurchaseStart(WSMessage): class WSProductPurchaseStartMessage(WSMessage):
message: str = "product_purchase_start" message: str = "product_purchase_start"
data: ProductPurchaseStartDto data: ProductPurchaseStartDto
class WSProductPurchaseDone(WSMessage): class WSProductPurchaseTimerTickMessage(WSMessage):
message: str = "product_purchase_timer_tick"
data: ProductPurchaseTimerDto
class WSProductPurchaseDoneMessage(WSMessage):
message: str = "product_purchase_done" message: str = "product_purchase_done"
data: ProductPurchaseDoneDto data: ProductPurchaseDoneDto

View File

@ -3,16 +3,15 @@ import datetime
import logging import logging
import time import time
from threading import Thread from threading import Thread
from typing import Optional
from hopper.interfaces import SendGameDumpInterface
from hopper.models.player import PlayerList from hopper.models.player import PlayerList
from hopper.ws_server import WSServer
from settings import settings from settings import settings
class InactivityWatchdog(Thread): class InactivityWatchdog(Thread):
def __init__( def __init__(
self, players: PlayerList, ws_server: Optional[SendGameDumpInterface] = None self, players: PlayerList, ws_server: WSServer = None
) -> None: ) -> None:
self.players = players self.players = players
self.ws_server = ws_server self.ws_server = ws_server
@ -61,8 +60,6 @@ class InactivityWatchdog(Thread):
self.send_game_dump() self.send_game_dump()
def send_game_dump(self): def send_game_dump(self):
if not self.ws_server:
return
logging.info("Sending WS game dump") logging.info("Sending WS game dump")
asyncio.run(self.ws_server.send_game_dump()) asyncio.run(self.ws_server.send_game_dump())

View File

@ -7,16 +7,20 @@ import websockets
from websockets import WebSocketServerProtocol from websockets import WebSocketServerProtocol
from websockets.exceptions import ConnectionClosedOK from websockets.exceptions import ConnectionClosedOK
from hopper.models.player import Player
from hopper.models.product import Product from hopper.models.product import Product
from hopper.models.ws_dto import ( from hopper.models.ws_dto import (
GameDumpDto, GameDumpDto,
ProductPurchaseDoneDto, ProductPurchaseDoneDto,
ProductPurchaseStartDto, ProductPurchaseStartDto,
ProductPurchaseTimerDto,
WSGameDumpMessage, WSGameDumpMessage,
WSMessage, WSMessage,
WSProductPurchaseDone, WSProductPurchaseDoneMessage,
WSProductPurchaseStart, WSProductPurchaseStartMessage,
WSProductPurchaseTimerTickMessage,
) )
from settings import settings
class WSServer(Thread): class WSServer(Thread):
@ -85,16 +89,35 @@ class WSServer(Thread):
message = self._create_game_dump_message() message = self._create_game_dump_message()
await self.send_message_to_clients(message) await self.send_message_to_clients(message)
async def send_product_purchase_message(self, products: Iterable[Product]) -> None: async def send_product_purchase_start_message(
message = WSProductPurchaseStart( self, player: Player, products: Iterable[Product]
data=ProductPurchaseStartDto(products=products) ) -> None:
message = WSProductPurchaseStartMessage(
data=ProductPurchaseStartDto(
player=player,
products=products,
timeout=settings.purchase_timeout,
)
)
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,
)
) )
await self.send_message_to_clients(message) await self.send_message_to_clients(message)
async def send_product_purchase_done_message( async def send_product_purchase_done_message(
self, product: Optional[Product] = None self, player: Player, product: Optional[Product] = None
) -> None: ) -> None:
message = WSProductPurchaseDone(data=ProductPurchaseDoneDto(product=product)) message = WSProductPurchaseDoneMessage(
data=ProductPurchaseDoneDto(player=player, product=product),
)
await self.send_message_to_clients(message) await self.send_message_to_clients(message)
async def run_async(self) -> None: async def run_async(self) -> None: