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

View File

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

View File

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

View File

@ -60,7 +60,6 @@ class GameEngine:
self._inacivity_watchdog = InactivityWatchdog( self._inacivity_watchdog = InactivityWatchdog(
players=self.players, players=self.players,
ws_server=self.ws_server, ws_server=self.ws_server,
daemon=True,
) )
self._inacivity_watchdog.start() self._inacivity_watchdog.start()
@ -144,20 +143,14 @@ class GameEngine:
if self._is_player_on_destination(player): if self._is_player_on_destination(player):
player.state = PlayerState.ON_DESTINATION player.state = PlayerState.ON_DESTINATION
self._player_on_destination(player) await self._player_on_destination(player)
logging.info(f"Player {player} reached destination!") return PlayerMoveResult.DESTINATION_REACHED
if self.ws_server: if self.ws_server:
await self.ws_server.send_game_dump() await self.ws_server.send_game_dump()
self.__debug_print_board() 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) await asyncio.sleep(settings.game.MOVE_DELAY)
return PlayerMoveResult.OK return PlayerMoveResult.OK
def _is_player_on_destination(self, player: Player) -> bool: def _is_player_on_destination(self, player: Player) -> bool:
@ -171,8 +164,15 @@ class GameEngine:
def _colided_with_obstacle(self, position: Position) -> bool: def _colided_with_obstacle(self, position: Position) -> bool:
return self.board.get_object_at_position(position) is not None 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 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") logging.info(f"Starting purchase countdown timer for {settings.purchase_timeout} seconds")
self._purchase_countdown_timer = CountdownTimer( self._purchase_countdown_timer = CountdownTimer(
seconds=settings.purchase_timeout, seconds=settings.purchase_timeout,
@ -183,6 +183,9 @@ class GameEngine:
def _on_purchase_timeout(self) -> None: def _on_purchase_timeout(self) -> 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(self.ws_server.send_product_purchase_done_message(product=None))
self.game_state = GameState.RUNNING self.game_state = GameState.RUNNING
def get_board_layout(self) -> BoardLayout: 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): class SendGameDumpInterface(Protocol):
async def send_game_dump(self) -> None: 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 from __future__ import annotations
import json import json
from typing import TypeVar, Generic from typing import Optional, TypeVar
from pydantic import Field from pydantic import Field
from pydantic.generics import GenericModel 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 from hopper.enums import ObjectType
from hopper.models.product import Product
class LayerObjectDto(BaseModel): class LayerObjectDto(BaseModel):
@ -20,6 +21,11 @@ class LayerDto(BaseModel):
objects: list[LayerObjectDto] objects: list[LayerObjectDto]
class ProductDto(BaseModel):
name: str
uuid: str
class GameDumpPlayerDto(PlayerDto): class GameDumpPlayerDto(PlayerDto):
... ...
@ -31,12 +37,20 @@ class GameDumpDto(BaseModel):
layers: list[LayerDto] layers: list[LayerDto]
class ProductPurchaseStartDto(BaseModel):
products: list[ProductDto]
class ProductPurchaseDoneDto(BaseModel):
product: Optional[ProductDto] = None
TMessageData = TypeVar("TMessageData", bound=BaseModel) TMessageData = TypeVar("TMessageData", bound=BaseModel)
class WSMessage(GenericModel): class WSMessage(GenericModel):
message: str message: str
data: TMessageData data: Optional[TMessageData] = None
def __str__(self) -> str: def __str__(self) -> str:
return self.to_str() return self.to_str()
@ -48,3 +62,13 @@ class WSMessage(GenericModel):
class WSGameDumpMessage(WSMessage): class WSGameDumpMessage(WSMessage):
message: str = "game_dump" message: str = "game_dump"
data: GameDumpDto 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): class InactivityWatchdog(Thread):
def __init__( def __init__(
self, self, players: PlayerList, ws_server: Optional[SendGameDumpInterface] = None
players: PlayerList,
ws_server: Optional[SendGameDumpInterface] = None,
*args,
**kwargs,
) -> None: ) -> None:
self.players = players self.players = players
self.ws_server = ws_server self.ws_server = ws_server
self.stopped = False self.stopped = False
super().__init__(*args, **kwargs) super().__init__(daemon=True)
def run(self) -> None: def run(self) -> None:
logging.info("Starting inactivity watchdog") logging.info("Starting inactivity watchdog")

View File

@ -1,16 +1,30 @@
import asyncio import asyncio
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 ConnectionClosedOK from websockets.exceptions import ConnectionClosedOK
from hopper.models.ws_dto import GameDumpDto, WSGameDumpMessage from hopper.models.product import Product
from settings import settings from hopper.models.ws_dto import (
GameDumpDto,
ProductPurchaseDoneDto,
ProductPurchaseStartDto,
WSGameDumpMessage,
WSMessage,
WSProductPurchaseDone,
WSProductPurchaseStart,
)
class WSServer(Thread): 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: 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)
@ -31,6 +45,19 @@ class WSServer(Thread):
self.connected_clients.remove(websocket) self.connected_clients.remove(websocket)
logging.info(f"Remove client: {websocket.id}") 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: def _create_game_dump_message(self) -> WSGameDumpMessage:
# avoid circular imports # avoid circular imports
from hopper.api.dependencies import get_game_engine from hopper.api.dependencies import get_game_engine
@ -55,25 +82,30 @@ class WSServer(Thread):
async def send_game_dump(self) -> None: async def send_game_dump(self) -> None:
"""Broadcast game state to all connected clients""" """Broadcast game state to all connected clients"""
if not self.connected_clients:
return
message = self._create_game_dump_message() message = self._create_game_dump_message()
logging.debug( await self.send_message_to_clients(message)
f"Sending game dump to clients: {self.connected_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 self.send_message_to_clients(message)
await client.send(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: async def run_async(self) -> None:
logging.info( 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( async with websockets.serve(
ws_handler=self.handler, ws_handler=self.handler,
host=settings.ws_server.HOST, host=self.host,
port=settings.ws_server.PORT, port=self.port,
): ):
await asyncio.Future() # run forever await asyncio.Future() # run forever