Player state

This commit is contained in:
Eden Kirin
2023-03-30 13:09:23 +02:00
parent 8a48d61dc9
commit ecffdc5d1e
11 changed files with 84 additions and 25 deletions

View File

@ -15,21 +15,25 @@ GET http://localhost:8010/game
### ###
# get player info # get player info
GET http://localhost:8010/player/test-player-id GET http://localhost:8010/player/test-player-pero
### ###
# move player left # move player left
POST http://localhost:8010/player/test-player-id/move/left POST http://localhost:8010/player/test-player-pero/move/left
### ###
# move player right # move player right
POST http://localhost:8010/player/test-player-id/move/right POST http://localhost:8010/player/test-player-pero/move/right
### ###
# move player up # move player up
POST http://localhost:8010/player/test-player-id/move/up POST http://localhost:8010/player/test-player-pero/move/up
### ###
# move player down # move player down
POST http://localhost:8010/player/test-player-id/move/down POST http://localhost:8010/player/test-player-pero/move/down
###
# move Mirko left
POST http://localhost:8010/player/test-player-mirko/move/left
### ###

View File

@ -70,10 +70,11 @@
function renderPlayerList(players) { function renderPlayerList(players) {
const html = players.filter(player => player.active).map((player) => { const html = players.filter(player => player.active).map((player) => {
const onDestination = player.state == "ON_DESTINATION";
return ` return `
<li class="${player.reached_destination ? "text-success" : ""}"> <li class="${onDestination ? "text-success" : ""}">
${player.name} (${player.move_count}) ${player.name} (${player.move_count})
${player.reached_destination ? "✅" : ""} ${onDestination ? "✅" : ""}
</li> </li>
`; `;
}).join(""); }).join("");
@ -83,7 +84,8 @@
function renderPlayers(players) { function renderPlayers(players) {
players.filter(player => player.active).forEach(player => { players.filter(player => player.active).forEach(player => {
const cell = findCell(player.position); const cell = findCell(player.position);
const playerIcon = player.reached_destination ? BOARD_ICONS.PLAYER_ON_DESTINATION : BOARD_ICONS.PLAYER; const onDestination = player.state == "ON_DESTINATION";
const playerIcon = onDestination ? BOARD_ICONS.PLAYER_ON_DESTINATION : BOARD_ICONS.PLAYER;
if (cell) { if (cell) {
const html = ` const html = `
<div class="player-tooltip">${player.name}</div> <div class="player-tooltip">${player.name}</div>

View File

@ -2,6 +2,8 @@ from __future__ import annotations
from pydantic import BaseModel as PydanticBaseModel from pydantic import BaseModel as PydanticBaseModel
from hopper.enums import PlayerState
class BaseModel(PydanticBaseModel): class BaseModel(PydanticBaseModel):
class Config: class Config:
@ -29,6 +31,7 @@ class PlayerDto(BaseModel):
position: PositionDto position: PositionDto
move_count: int move_count: int
move_attempt_count: int move_attempt_count: int
state: PlayerState
class DestinationDto(BaseModel): class DestinationDto(BaseModel):

View File

@ -14,7 +14,7 @@ from hopper.api.dto import (
) )
from hopper.engine import GameEngine from hopper.engine import GameEngine
from hopper.enums import Direction, PlayerMoveResult from hopper.enums import Direction, PlayerMoveResult
from hopper.errors import Collision, PositionOutOfBounds from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds
from hopper.models.player import Player from hopper.models.player import Player
router = APIRouter() router = APIRouter()
@ -60,7 +60,7 @@ async def start_game(
body: StartGameRequestDto, body: StartGameRequestDto,
engine: GameEngine = Depends(get_game_engine), engine: GameEngine = Depends(get_game_engine),
) -> StartGameResponseDto: ) -> StartGameResponseDto:
new_player = await engine.start_game(player_name=body.player_name) new_player = await engine.start_game_for_player(player_name=body.player_name)
return StartGameResponseDto( return StartGameResponseDto(
board=engine.board, board=engine.board,
@ -112,6 +112,10 @@ async def get_player_info(
"model": ErrorResponseDto, "model": ErrorResponseDto,
"description": " Position out of bounds or collision with an object", "description": " Position out of bounds or collision with an object",
}, },
status.HTTP_423_LOCKED: {
"model": ErrorResponseDto,
"description": " Player reached destination. Can't move anymore.",
},
}, },
) )
async def move_player( async def move_player(
@ -130,6 +134,11 @@ async def move_player(
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collision with an object" status_code=status.HTTP_409_CONFLICT, detail="Collision with an object"
) )
except GameLockForMovement:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail="Player reached destination. Can't move anymore.",
)
if move_result == PlayerMoveResult.DESTINATION_REACHED: if move_result == PlayerMoveResult.DESTINATION_REACHED:
response.status_code = status.HTTP_200_OK response.status_code = status.HTTP_200_OK

25
hopper/countdown_timer.py Normal file
View File

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

View File

@ -3,7 +3,8 @@ import logging
import random import random
from typing import Optional from typing import Optional
from hopper.enums import Direction, PlayerMoveResult, GameState from hopper.countdown_timer import CountdownTimer
from hopper.enums import Direction, PlayerMoveResult, GameState, PlayerState
from hopper.errors import Collision, PositionOutOfBounds, GameLockForMovement from hopper.errors import Collision, PositionOutOfBounds, GameLockForMovement
from hopper.interfaces import SendGameDumpInterface from hopper.interfaces import SendGameDumpInterface
from hopper.models.board import ( from hopper.models.board import (
@ -29,8 +30,8 @@ class GameEngine:
self.ws_server = ws_server self.ws_server = ws_server
self.players = PlayerList() self.players = PlayerList()
self._inacivity_watchdog = None self._inacivity_watchdog = None
self.__debug_print_board() self._purchase_countdown_timer: Optional[CountdownTimer] = None
self.game_state = GameState.RUNNING self.reset_game()
def dump_board(self) -> list[list[str]]: def dump_board(self) -> list[list[str]]:
dump = self.board.dump() dump = self.board.dump()
@ -63,11 +64,16 @@ class GameEngine:
) )
self._inacivity_watchdog.start() self._inacivity_watchdog.start()
async def start_game(self, player_name: str) -> Player: async def reset_game(self) -> None:
self.__debug_print_board()
self.game_state = GameState.RUNNING
async def start_game_for_player(self, player_name: str) -> Player:
self._start_inactivity_watchdog() self._start_inactivity_watchdog()
player = Player( player = Player(
name=player_name, name=player_name,
position=self._create_player_start_position(), position=self._create_player_start_position(),
state=PlayerState.CREATED,
) )
self.players.append(player) self.players.append(player)
@ -115,17 +121,17 @@ class GameEngine:
player.reset_timeout() player.reset_timeout()
if self.game_state == GameState.LOCK_FOR_MOVEMENT: if self.game_state == GameState.LOCK_FOR_MOVEMENT:
raise GameLockForMovement("Player reached destination. Can't move now.") raise GameLockForMovement("Player reached destination. Can't move anymore.")
# player will not be able to move once they reach the destination # player will not be able to move once they reach the destination
if player.reached_destination: if player.state == PlayerState.ON_DESTINATION:
self.game_state = GameState.LOCK_FOR_MOVEMENT
return PlayerMoveResult.DESTINATION_REACHED return PlayerMoveResult.DESTINATION_REACHED
logging.info(f"Player {player} move to {direction}") logging.info(f"Player {player} move to {direction}")
new_position = self._move_position(player.position, direction) new_position = self._move_position(player.position, direction)
player.move_attempt_count += 1 player.move_attempt_count += 1
player.state = PlayerState.MOVING
if not self._position_in_board_bounds(new_position): if not self._position_in_board_bounds(new_position):
raise PositionOutOfBounds() raise PositionOutOfBounds()
@ -137,7 +143,7 @@ class GameEngine:
player.move_count += 1 player.move_count += 1
if self._is_player_on_destination(player): if self._is_player_on_destination(player):
player.reached_destination = True player.state = PlayerState.ON_DESTINATION
logging.info(f"Player {player} reached destination!") logging.info(f"Player {player} reached destination!")
if self.ws_server: if self.ws_server:
@ -145,7 +151,8 @@ class GameEngine:
self.__debug_print_board() self.__debug_print_board()
if player.reached_destination: if player.state == PlayerState.ON_DESTINATION:
self.game_state = GameState.LOCK_FOR_MOVEMENT
return PlayerMoveResult.DESTINATION_REACHED return PlayerMoveResult.DESTINATION_REACHED
await asyncio.sleep(settings.game.MOVE_DELAY) await asyncio.sleep(settings.game.MOVE_DELAY)
@ -163,6 +170,10 @@ 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:
self.game_state = GameState.LOCK_FOR_MOVEMENT
self._purchase_countdown_timer = CountdownTimer()
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)

View File

@ -26,7 +26,8 @@ class GameState(Enum):
ENDGAME = auto() ENDGAME = auto()
class PlayerState(Enum): class PlayerState(str, Enum):
PLAYING = auto() CREATED = "CREATED"
ON_DESTINATION = auto() MOVING = "MOVING"
INACTIVE = auto() ON_DESTINATION = "ON_DESTINATION"
INACTIVE = "INACTIVE"

View File

@ -42,5 +42,6 @@ class Settings:
board: BoardSettings board: BoardSettings
inacivity_watchdog: InactivityWatchdogSettings inacivity_watchdog: InactivityWatchdogSettings
ws_server: WSServerSettings ws_server: WSServerSettings
purchase_timeout: int = 10 # seconds
log_level: int = logging.INFO log_level: int = logging.INFO
debug: Optional[DebugSettings] = None debug: Optional[DebugSettings] = None

View File

@ -3,6 +3,8 @@ import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from hopper.enums import PlayerState
@dataclass @dataclass
class Position: class Position:
@ -20,9 +22,9 @@ class Player:
last_seen: datetime.datetime = field( last_seen: datetime.datetime = field(
default_factory=lambda: datetime.datetime.now() default_factory=lambda: datetime.datetime.now()
) )
state: PlayerState = PlayerState.CREATED
active: bool = True active: bool = True
can_be_deactivated: bool = True can_be_deactivated: bool = True
reached_destination: bool = False
def reset_timeout(self) -> None: def reset_timeout(self) -> None:
self.last_seen = datetime.datetime.now() self.last_seen = datetime.datetime.now()

View File

@ -16,7 +16,7 @@ class LayerDto(BaseModel):
objects: list[LayerObjectDto] objects: list[LayerObjectDto]
class GameDumpPlayerDto(PlayerDto): class GameDumpPlayerDto(PlayerDto):
reached_destination: bool ...
class GameDumpDto(BaseModel): class GameDumpDto(BaseModel):

View File

@ -14,5 +14,6 @@ settings = Settings(
inacivity_watchdog=InactivityWatchdogSettings(), inacivity_watchdog=InactivityWatchdogSettings(),
log_level=logging.INFO, log_level=logging.INFO,
ws_server=WSServerSettings(), ws_server=WSServerSettings(),
purchase_timeout=10,
debug=None, debug=None,
) )