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

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

View File

@ -14,7 +14,7 @@ from hopper.api.dto import (
)
from hopper.engine import GameEngine
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
router = APIRouter()
@ -60,7 +60,7 @@ async def start_game(
body: StartGameRequestDto,
engine: GameEngine = Depends(get_game_engine),
) -> 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(
board=engine.board,
@ -112,6 +112,10 @@ async def get_player_info(
"model": ErrorResponseDto,
"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(
@ -130,6 +134,11 @@ async def move_player(
raise HTTPException(
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:
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
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.interfaces import SendGameDumpInterface
from hopper.models.board import (
@ -29,8 +30,8 @@ class GameEngine:
self.ws_server = ws_server
self.players = PlayerList()
self._inacivity_watchdog = None
self.__debug_print_board()
self.game_state = GameState.RUNNING
self._purchase_countdown_timer: Optional[CountdownTimer] = None
self.reset_game()
def dump_board(self) -> list[list[str]]:
dump = self.board.dump()
@ -63,11 +64,16 @@ class GameEngine:
)
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()
player = Player(
name=player_name,
position=self._create_player_start_position(),
state=PlayerState.CREATED,
)
self.players.append(player)
@ -115,17 +121,17 @@ class GameEngine:
player.reset_timeout()
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
if player.reached_destination:
self.game_state = GameState.LOCK_FOR_MOVEMENT
if player.state == PlayerState.ON_DESTINATION:
return PlayerMoveResult.DESTINATION_REACHED
logging.info(f"Player {player} move to {direction}")
new_position = self._move_position(player.position, direction)
player.move_attempt_count += 1
player.state = PlayerState.MOVING
if not self._position_in_board_bounds(new_position):
raise PositionOutOfBounds()
@ -137,7 +143,7 @@ class GameEngine:
player.move_count += 1
if self._is_player_on_destination(player):
player.reached_destination = True
player.state = PlayerState.ON_DESTINATION
logging.info(f"Player {player} reached destination!")
if self.ws_server:
@ -145,7 +151,8 @@ class GameEngine:
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
await asyncio.sleep(settings.game.MOVE_DELAY)
@ -163,6 +170,10 @@ 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:
self.game_state = GameState.LOCK_FOR_MOVEMENT
self._purchase_countdown_timer = CountdownTimer()
def get_board_layout(self) -> BoardLayout:
return BoardLayout(board=self.board, players=self.players)

View File

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

View File

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

View File

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

View File

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