Compare commits
2 Commits
6111d07f09
...
c9707c0523
| Author | SHA1 | Date | |
|---|---|---|---|
| c9707c0523 | |||
| 059408242c |
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
...
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user