2 Commits

Author SHA1 Message Date
c9707c0523 Threads creation optimization 2023-03-30 21:13:54 +02:00
059408242c Send purchase state 2023-03-30 21:09:11 +02:00
8 changed files with 124 additions and 34 deletions

View File

@ -123,6 +123,18 @@
renderPlayers(data.players);
}
function productPurchaseStart(products) {
console.log("productPurchaseStart:", products)
}
function productPurchased(product) {
console.log("productPurchased:", product)
}
function productPurchaseDone() {
console.log("productPurchaseDone")
}
function wsConnect() {
let ws = new WebSocket('ws://localhost:8011');
ws.onopen = () => {
@ -137,6 +149,15 @@
case "game_dump":
renderGameDump(wsMessage.data);
break;
case "product_purchase_start":
productPurchaseStart(wsMessage.data)
break;
case "product_purchased":
productPurchased(wsMessage.data)
break;
case "product_purchase_done":
productPurchaseDone()
break;
default:
console.error("Unknown message:", wsMessage)
}

View File

@ -2,6 +2,7 @@ from typing import Optional
from hopper.engine import GameEngine, GameEngineFactory
from hopper.ws_server import WSServer
from settings import settings
game_engine: Optional[GameEngine] = None
@ -12,7 +13,10 @@ def create_game_engine() -> GameEngine:
if game_engine:
raise RuntimeError("Can't call create_game_engine() more than once!")
ws_server = WSServer(daemon=True)
ws_server = WSServer(
host=settings.ws_server.HOST,
port=settings.ws_server.PORT,
)
ws_server.start()
game_engine = GameEngineFactory.create_default(ws_server=ws_server)

View File

@ -10,11 +10,11 @@ class CountdownTimer(Thread):
self.seconds = seconds
self.stop_event = Event()
self.callback = callback
super().__init__()
super().__init__(daemon=True)
def run(self) -> None:
cnt = self.seconds
while cnt > 0 and not self.stop_event.is_set():
while cnt and not self.stop_event.is_set():
cnt -= 1
time.sleep(1)

View File

@ -60,7 +60,6 @@ class GameEngine:
self._inacivity_watchdog = InactivityWatchdog(
players=self.players,
ws_server=self.ws_server,
daemon=True,
)
self._inacivity_watchdog.start()
@ -144,20 +143,14 @@ class GameEngine:
if self._is_player_on_destination(player):
player.state = PlayerState.ON_DESTINATION
self._player_on_destination(player)
logging.info(f"Player {player} reached destination!")
await self._player_on_destination(player)
return PlayerMoveResult.DESTINATION_REACHED
if self.ws_server:
await self.ws_server.send_game_dump()
self.__debug_print_board()
if player.state == PlayerState.ON_DESTINATION:
self.game_state = GameState.LOCK_FOR_MOVEMENT
return PlayerMoveResult.DESTINATION_REACHED
await asyncio.sleep(settings.game.MOVE_DELAY)
return PlayerMoveResult.OK
def _is_player_on_destination(self, player: Player) -> bool:
@ -171,8 +164,15 @@ class GameEngine:
def _colided_with_obstacle(self, position: Position) -> bool:
return self.board.get_object_at_position(position) is not None
def _player_on_destination(self, player: Player) -> None:
async def _player_on_destination(self, player: Player) -> None:
logging.info(f"Player {player} reached destination!")
self.game_state = GameState.LOCK_FOR_MOVEMENT
await self.ws_server.send_game_dump()
self.__debug_print_board()
await self.ws_server.send_product_purchase_message(products=settings.products)
logging.info(f"Starting purchase countdown timer for {settings.purchase_timeout} seconds")
self._purchase_countdown_timer = CountdownTimer(
seconds=settings.purchase_timeout,
@ -183,6 +183,9 @@ class GameEngine:
def _on_purchase_timeout(self) -> None:
logging.info("Ding ding! Purchase countdown timer timeout")
self._purchase_countdown_timer = None
asyncio.run(self.ws_server.send_product_purchase_done_message(product=None))
self.game_state = GameState.RUNNING
def get_board_layout(self) -> BoardLayout:

View File

@ -1,6 +1,16 @@
from typing import Protocol
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

@ -1,13 +1,14 @@
from __future__ import annotations
import json
from typing import TypeVar, Generic
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.enums import ObjectType
from hopper.models.product import Product
class LayerObjectDto(BaseModel):
@ -20,6 +21,11 @@ class LayerDto(BaseModel):
objects: list[LayerObjectDto]
class ProductDto(BaseModel):
name: str
uuid: str
class GameDumpPlayerDto(PlayerDto):
...
@ -31,12 +37,20 @@ class GameDumpDto(BaseModel):
layers: list[LayerDto]
class ProductPurchaseStartDto(BaseModel):
products: list[ProductDto]
class ProductPurchaseDoneDto(BaseModel):
product: Optional[ProductDto] = None
TMessageData = TypeVar("TMessageData", bound=BaseModel)
class WSMessage(GenericModel):
message: str
data: TMessageData
data: Optional[TMessageData] = None
def __str__(self) -> str:
return self.to_str()
@ -48,3 +62,13 @@ class WSMessage(GenericModel):
class WSGameDumpMessage(WSMessage):
message: str = "game_dump"
data: GameDumpDto
class WSProductPurchaseStart(WSMessage):
message: str = "product_purchase_start"
data: ProductPurchaseStartDto
class WSProductPurchaseDone(WSMessage):
message: str = "product_purchase_done"
data: ProductPurchaseDoneDto

View File

@ -12,16 +12,12 @@ from settings import settings
class InactivityWatchdog(Thread):
def __init__(
self,
players: PlayerList,
ws_server: Optional[SendGameDumpInterface] = None,
*args,
**kwargs,
self, players: PlayerList, ws_server: Optional[SendGameDumpInterface] = None
) -> None:
self.players = players
self.ws_server = ws_server
self.stopped = False
super().__init__(*args, **kwargs)
super().__init__(daemon=True)
def run(self) -> None:
logging.info("Starting inactivity watchdog")

View File

@ -1,16 +1,30 @@
import asyncio
import logging
from threading import Thread
from typing import Iterable, Optional
import websockets
from websockets import WebSocketServerProtocol
from websockets.exceptions import ConnectionClosedOK
from hopper.models.ws_dto import GameDumpDto, WSGameDumpMessage
from settings import settings
from hopper.models.product import Product
from hopper.models.ws_dto import (
GameDumpDto,
ProductPurchaseDoneDto,
ProductPurchaseStartDto,
WSGameDumpMessage,
WSMessage,
WSProductPurchaseDone,
WSProductPurchaseStart,
)
class WSServer(Thread):
def __init__(self, host: str, port: int) -> None:
self.host = host
self.port = port
super().__init__(daemon=True)
async def handler(self, websocket: WebSocketServerProtocol) -> None:
"""New handler instance spawns for each connected client"""
self.connected_clients.add(websocket)
@ -31,6 +45,19 @@ class WSServer(Thread):
self.connected_clients.remove(websocket)
logging.info(f"Remove client: {websocket.id}")
async def send_message_to_client(
self, client: WebSocketServerProtocol, message: WSMessage
) -> None:
message_str = message.to_str()
logging.debug(
f"Sending message {message.message} to clients: {self.connected_clients}: {message_str}"
)
await client.send(message_str)
async def send_message_to_clients(self, message: WSMessage) -> None:
for client in self.connected_clients:
await self.send_message_to_client(client, message)
def _create_game_dump_message(self) -> WSGameDumpMessage:
# avoid circular imports
from hopper.api.dependencies import get_game_engine
@ -55,25 +82,30 @@ class WSServer(Thread):
async def send_game_dump(self) -> None:
"""Broadcast game state to all connected clients"""
if not self.connected_clients:
return
message = self._create_game_dump_message()
logging.debug(
f"Sending game dump to clients: {self.connected_clients}: {message}"
await self.send_message_to_clients(message)
async def send_product_purchase_message(self, products: Iterable[Product]) -> None:
message = WSProductPurchaseStart(
data=ProductPurchaseStartDto(products=products)
)
for client in self.connected_clients:
await client.send(message)
await self.send_message_to_clients(message)
async def send_product_purchase_done_message(
self, product: Optional[Product] = None
) -> None:
message = WSProductPurchaseDone(data=ProductPurchaseDoneDto(product=product))
await self.send_message_to_clients(message)
async def run_async(self) -> None:
logging.info(
f"Starting FairHopper Websockets Server on {settings.ws_server.HOST}:{settings.ws_server.PORT}"
f"Starting FairHopper Websockets Server on {self.host}:{self.port}"
)
async with websockets.serve(
ws_handler=self.handler,
host=settings.ws_server.HOST,
port=settings.ws_server.PORT,
host=self.host,
port=self.port,
):
await asyncio.Future() # run forever