diff --git a/api_tests/requests.http b/api_tests/requests.http
index 4043131..cfc5543 100644
--- a/api_tests/requests.http
+++ b/api_tests/requests.http
@@ -15,21 +15,25 @@ GET http://localhost:8010/game
###
# get player info
-GET http://localhost:8010/player/test-player-id
+GET http://localhost:8010/player/test-player-pero
###
# 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
-POST http://localhost:8010/player/test-player-id/move/right
+POST http://localhost:8010/player/test-player-pero/move/right
###
# 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
-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
###
diff --git a/frontend/index.html b/frontend/index.html
index 239d4b4..6a4c50a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -70,10 +70,11 @@
function renderPlayerList(players) {
const html = players.filter(player => player.active).map((player) => {
+ const onDestination = player.state == "ON_DESTINATION";
return `
-
+
${player.name} (${player.move_count})
- ${player.reached_destination ? "✅" : ""}
+ ${onDestination ? "✅" : ""}
`;
}).join("");
@@ -83,7 +84,8 @@
function renderPlayers(players) {
players.filter(player => player.active).forEach(player => {
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) {
const html = `
${player.name}
diff --git a/hopper/api/dto.py b/hopper/api/dto.py
index 4e34537..5d4cb18 100644
--- a/hopper/api/dto.py
+++ b/hopper/api/dto.py
@@ -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):
diff --git a/hopper/api/views.py b/hopper/api/views.py
index e6a31cf..00d1d5d 100644
--- a/hopper/api/views.py
+++ b/hopper/api/views.py
@@ -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
diff --git a/hopper/countdown_timer.py b/hopper/countdown_timer.py
new file mode 100644
index 0000000..c7786c0
--- /dev/null
+++ b/hopper/countdown_timer.py
@@ -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()
diff --git a/hopper/engine.py b/hopper/engine.py
index 7654c00..4f9e51e 100644
--- a/hopper/engine.py
+++ b/hopper/engine.py
@@ -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)
diff --git a/hopper/enums.py b/hopper/enums.py
index 5b37e9d..26586ba 100644
--- a/hopper/enums.py
+++ b/hopper/enums.py
@@ -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"
diff --git a/hopper/models/config.py b/hopper/models/config.py
index 5dfb3a3..85141b6 100644
--- a/hopper/models/config.py
+++ b/hopper/models/config.py
@@ -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
diff --git a/hopper/models/player.py b/hopper/models/player.py
index ce6c053..5d5010d 100644
--- a/hopper/models/player.py
+++ b/hopper/models/player.py
@@ -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()
diff --git a/hopper/models/ws_dto.py b/hopper/models/ws_dto.py
index c6fd72a..40bead7 100644
--- a/hopper/models/ws_dto.py
+++ b/hopper/models/ws_dto.py
@@ -16,7 +16,7 @@ class LayerDto(BaseModel):
objects: list[LayerObjectDto]
class GameDumpPlayerDto(PlayerDto):
- reached_destination: bool
+ ...
class GameDumpDto(BaseModel):
diff --git a/settings_template.py b/settings_template.py
index b80c4ba..e1ad9c9 100644
--- a/settings_template.py
+++ b/settings_template.py
@@ -14,5 +14,6 @@ settings = Settings(
inacivity_watchdog=InactivityWatchdogSettings(),
log_level=logging.INFO,
ws_server=WSServerSettings(),
+ purchase_timeout=10,
debug=None,
)