Integrated WS server
This commit is contained in:
@ -1,11 +1,22 @@
|
||||
from hopper.engine import GameEngine, GameEngineFactory
|
||||
from typing import Optional
|
||||
|
||||
game_engine: GameEngine
|
||||
from hopper.engine import GameEngine, GameEngineFactory
|
||||
from hopper.ws_server import WSServer
|
||||
|
||||
game_engine: Optional[GameEngine] = None
|
||||
|
||||
|
||||
def create_game_engine() -> GameEngine:
|
||||
global game_engine
|
||||
game_engine = GameEngineFactory.create_default()
|
||||
|
||||
if game_engine:
|
||||
raise RuntimeError("Can't call create_game_engine() more than once!")
|
||||
|
||||
ws_server = WSServer(daemon=True)
|
||||
ws_server.start()
|
||||
|
||||
game_engine = GameEngineFactory.create_default(ws_server=ws_server)
|
||||
|
||||
return game_engine
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ from hopper.api.dto import (
|
||||
from hopper.engine import GameEngine
|
||||
from hopper.enums import Direction, PlayerMoveResult
|
||||
from hopper.errors import Collision, PositionOutOfBounds
|
||||
from hopper.ws_client import ws_send_game_state
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from hopper.enums import Direction, PlayerMoveResult
|
||||
from hopper.errors import Collision, PositionOutOfBounds
|
||||
from hopper.models.board import (
|
||||
BOARD_DUMP_CHARS,
|
||||
BoardLayout,
|
||||
Destination,
|
||||
GameBoard,
|
||||
Layer,
|
||||
LayerObject,
|
||||
ObjectType,
|
||||
create_random_position, BoardLayout,
|
||||
create_random_position,
|
||||
)
|
||||
from hopper.models.player import Player, PlayerList, Position
|
||||
from hopper.watchdog import InactivityWatchdog
|
||||
from hopper.ws_client import ws_send_game_state
|
||||
from hopper.ws_server import WSServer
|
||||
from settings import settings
|
||||
|
||||
|
||||
class GameEngine:
|
||||
def __init__(self, board: GameBoard) -> None:
|
||||
def __init__(self, board: GameBoard, ws_server: Optional[WSServer] = None) -> None:
|
||||
self.board = board
|
||||
self.ws_server = ws_server
|
||||
self.players = PlayerList()
|
||||
self._inacivity_watchdog = None
|
||||
self.__debug_print_board()
|
||||
@ -59,11 +62,14 @@ class GameEngine:
|
||||
logging.info(f"Starting new game for player: {player}")
|
||||
self.__debug_print_board()
|
||||
|
||||
await ws_send_game_state()
|
||||
if self.ws_server:
|
||||
await self.ws_server.send_game_state()
|
||||
|
||||
return player
|
||||
|
||||
async def move_player(self, player: Player, direction: Direction) -> PlayerMoveResult:
|
||||
async def move_player(
|
||||
self, player: Player, direction: Direction
|
||||
) -> PlayerMoveResult:
|
||||
player.reset_timeout()
|
||||
|
||||
new_position = Position(player.position.x, player.position.y)
|
||||
@ -91,7 +97,8 @@ class GameEngine:
|
||||
player.position = new_position
|
||||
player.move_count += 1
|
||||
|
||||
await ws_send_game_state()
|
||||
if self.ws_server:
|
||||
await self.ws_server.send_game_state()
|
||||
|
||||
if self.is_player_on_destination(player):
|
||||
logging.info(f"Player {player} reached destination!")
|
||||
@ -116,12 +123,14 @@ class GameEngine:
|
||||
def get_board_layout(self) -> BoardLayout:
|
||||
return BoardLayout(board=self.board, players=self.players)
|
||||
|
||||
|
||||
class GameEngineFactory:
|
||||
@staticmethod
|
||||
def create(
|
||||
board_width: int,
|
||||
board_height: int,
|
||||
obstacle_count: int = 0,
|
||||
ws_server: Optional[WSServer] = None,
|
||||
) -> GameEngine:
|
||||
board = GameBoard(
|
||||
width=board_width,
|
||||
@ -138,16 +147,20 @@ class GameEngineFactory:
|
||||
)
|
||||
board.layers.append(obstacle_layer)
|
||||
|
||||
game = GameEngine(board=board)
|
||||
game = GameEngine(
|
||||
board=board,
|
||||
ws_server=ws_server,
|
||||
)
|
||||
GameEngineFactory.__add_test_player(game.players)
|
||||
return game
|
||||
|
||||
@staticmethod
|
||||
def create_default() -> GameEngine:
|
||||
def create_default(ws_server: Optional[WSServer] = None) -> GameEngine:
|
||||
return GameEngineFactory.create(
|
||||
board_width=settings.board.WIDTH,
|
||||
board_height=settings.board.HEIGHT,
|
||||
obstacle_count=settings.board.OBSTACLE_COUNT,
|
||||
ws_server=ws_server,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -3,15 +3,19 @@ import datetime
|
||||
import logging
|
||||
import time
|
||||
from threading import Thread
|
||||
from typing import Optional
|
||||
|
||||
from hopper.models.player import PlayerList
|
||||
from hopper.ws_client import ws_send_game_state
|
||||
from hopper.ws_server import WSServer
|
||||
from settings import settings
|
||||
|
||||
|
||||
class InactivityWatchdog(Thread):
|
||||
def __init__(self, players: PlayerList, *args, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, players: PlayerList, ws_server: Optional[WSServer] = None, *args, **kwargs
|
||||
) -> None:
|
||||
self.players = players
|
||||
self.ws_server = ws_server
|
||||
self.stopped = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@ -57,8 +61,10 @@ class InactivityWatchdog(Thread):
|
||||
self.send_game_state()
|
||||
|
||||
def send_game_state(self):
|
||||
if not self.ws_server:
|
||||
return
|
||||
logging.info("Sending WS game state")
|
||||
asyncio.run(ws_send_game_state())
|
||||
asyncio.run(self.ws_server.send_game_state())
|
||||
|
||||
def stop(self) -> None:
|
||||
self.stopped = True
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import websockets
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
from hopper.models.ws_dto import GameStateDto
|
||||
from settings import settings
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_ws_client(path: str) -> websockets.WebSocketServerProtocol:
|
||||
ws_uri = f"ws://{settings.ws_server.HOST}:{settings.ws_server.PORT}{path}"
|
||||
async with websockets.connect(uri=ws_uri) as websocket:
|
||||
yield websocket
|
||||
|
||||
|
||||
async def ws_send_game_state() -> None:
|
||||
# avoid circular imports
|
||||
from hopper.api.dependencies import get_game_engine
|
||||
|
||||
try:
|
||||
async with create_ws_client("/game-state") as websocket:
|
||||
engine = get_game_engine()
|
||||
|
||||
game_state = GameStateDto(
|
||||
board=engine.board,
|
||||
destination=engine.board.destination,
|
||||
players=engine.players,
|
||||
layers=engine.get_board_layout().layers,
|
||||
)
|
||||
await websocket.send(json.dumps(game_state.dict()))
|
||||
except (OSError, ConnectionClosed) as ex:
|
||||
logging.error(f"Error sending WS state: {ex}")
|
||||
73
hopper/ws_server.py
Normal file
73
hopper/ws_server.py
Normal file
@ -0,0 +1,73 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
import websockets
|
||||
from websockets import WebSocketServerProtocol
|
||||
|
||||
from hopper.models.ws_dto import GameStateDto
|
||||
from settings import settings
|
||||
|
||||
|
||||
class WSServer(Thread):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self.connected_clients = set[WebSocketServerProtocol]()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def ws_handler(self, websocket: WebSocketServerProtocol) -> None:
|
||||
self.connected_clients.add(websocket)
|
||||
logging.info(f"Add client: {websocket.id}")
|
||||
|
||||
try:
|
||||
await self.send_game_state_to_client(websocket)
|
||||
finally:
|
||||
self.connected_clients.remove(websocket)
|
||||
logging.info(f"Remove client: {websocket.id}")
|
||||
|
||||
def _create_game_state_message(self) -> str:
|
||||
# avoid circular imports
|
||||
from hopper.api.dependencies import get_game_engine
|
||||
|
||||
engine = get_game_engine()
|
||||
|
||||
game_state = GameStateDto(
|
||||
board=engine.board,
|
||||
destination=engine.board.destination,
|
||||
players=engine.players,
|
||||
layers=engine.get_board_layout().layers,
|
||||
)
|
||||
return json.dumps(game_state.dict())
|
||||
|
||||
async def send_game_state_to_client(
|
||||
self, websocket: WebSocketServerProtocol
|
||||
) -> None:
|
||||
message = self._create_game_state_message()
|
||||
logging.debug(f"Sending game state to client: {websocket.id}")
|
||||
await websocket.send(message)
|
||||
|
||||
async def send_game_state(self) -> None:
|
||||
if not self.connected_clients:
|
||||
return
|
||||
|
||||
message = self._create_game_state_message()
|
||||
logging.debug(
|
||||
f"Sending game state to clients: {self.connected_clients}: {message}"
|
||||
)
|
||||
for client in self.connected_clients:
|
||||
await client.send(message)
|
||||
|
||||
async def run_async(self) -> None:
|
||||
logging.info(
|
||||
f"Starting FairHopper Websockets Server on {settings.ws_server.HOST}:{settings.ws_server.PORT}"
|
||||
)
|
||||
|
||||
async with websockets.serve(
|
||||
ws_handler=self.ws_handler,
|
||||
host=settings.ws_server.HOST,
|
||||
port=settings.ws_server.PORT,
|
||||
):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
def run(self) -> None:
|
||||
asyncio.run(self.run_async())
|
||||
Reference in New Issue
Block a user