diff --git a/api_tests/requests.http b/api_tests/requests.http index cfc5543..e2855a5 100644 --- a/api_tests/requests.http +++ b/api_tests/requests.http @@ -34,6 +34,15 @@ POST http://localhost:8010/player/test-player-pero/move/up POST http://localhost:8010/player/test-player-pero/move/down ### +# purchase product +POST http://localhost:8010/player/test-player-pero/product/purchase +Content-Type: application/json + +{ + "product_uuid": "cocacola-id" +} +### + # move Mirko left POST http://localhost:8010/player/test-player-mirko/move/left ### diff --git a/hopper/api/dto.py b/hopper/api/dto.py index 5d4cb18..9cc9c08 100644 --- a/hopper/api/dto.py +++ b/hopper/api/dto.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + from pydantic import BaseModel as PydanticBaseModel from hopper.enums import PlayerState @@ -38,6 +40,11 @@ class DestinationDto(BaseModel): position: PositionDto +class ProductDto(BaseModel): + name: str + uuid: str + description: Optional[str] = None + class StartGameRequestDto(BaseModel): player_name: str @@ -61,3 +68,11 @@ class PlayerInfoResponseDto(MovePlayerResponseDto): class ErrorResponseDto(BaseModel): detail: str + + +class GetProductsResponse(BaseModel): + products: list[ProductDto] + + +class PurchaseProductDto(BaseModel): + product_uuid: str diff --git a/hopper/api/views.py b/hopper/api/views.py index 00d1d5d..9aec4f7 100644 --- a/hopper/api/views.py +++ b/hopper/api/views.py @@ -6,16 +6,20 @@ from hopper.api.dto import ( DestinationDto, ErrorResponseDto, GameInfoDto, + GetProductsResponse, MovePlayerResponseDto, PingResponse, PlayerInfoResponseDto, + ProductDto, + PurchaseProductDto, StartGameRequestDto, StartGameResponseDto, ) from hopper.engine import GameEngine from hopper.enums import Direction, PlayerMoveResult -from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds +from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds, PurchaseForbiddenForPlayer from hopper.models.player import Player +from settings import settings router = APIRouter() @@ -144,3 +148,42 @@ async def move_player( response.status_code = status.HTTP_200_OK return MovePlayerResponseDto(player=player) + + +@router.get("/products", response_model=GetProductsResponse) +async def get_products() -> GetProductsResponse: + return GetProductsResponse( + products=settings.products, + ) + + +@router.get("/products/{uuid}", response_model=ProductDto) +async def get_product(uuid: str) -> ProductDto: + for product in settings.products: + if product.uuid == uuid: + return ProductDto.from_orm(product) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Product not found" + ) + + +@router.post("/player/{uuid}/product/purchase") +async def purchase_product( + body: PurchaseProductDto, + engine: GameEngine = Depends(get_game_engine), + player: Player = Depends(get_player), +): + for product in settings.products: + if product.uuid == body.product_uuid: + try: + await engine.purchase_product(player=player, product=product) + except PurchaseForbiddenForPlayer: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Purchase forbidden for this player", + ) + break + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Product not found" + ) diff --git a/hopper/countdown_timer.py b/hopper/countdown_timer.py index 90d0e90..0b11636 100644 --- a/hopper/countdown_timer.py +++ b/hopper/countdown_timer.py @@ -5,7 +5,10 @@ from typing import Callable, Optional class CountdownTimer(Thread): def __init__( - self, seconds: int, timer_tick_callback: Optional[Callable[[int], None]] = None, timer_done_callback: Optional[Callable[[], None]] = None + self, + seconds: int, + timer_tick_callback: Optional[Callable[[int], None]] = None, + timer_done_callback: Optional[Callable[[], None]] = None, ) -> None: self.seconds = seconds self.stop_event = Event() @@ -18,7 +21,7 @@ class CountdownTimer(Thread): while time_left and not self.stop_event.is_set(): time.sleep(1) time_left -= 1 - if self.timer_tick_callback: + if self.timer_tick_callback and not self.stop_event.is_set(): self.timer_tick_callback(time_left) if time_left == 0 and self.timer_done_callback: diff --git a/hopper/engine.py b/hopper/engine.py index 06b21c8..e2d3ce5 100644 --- a/hopper/engine.py +++ b/hopper/engine.py @@ -5,7 +5,12 @@ from typing import Optional from hopper.countdown_timer import CountdownTimer from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState -from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds +from hopper.errors import ( + Collision, + GameLockForMovement, + PositionOutOfBounds, + PurchaseForbiddenForPlayer, +) from hopper.models.board import ( BOARD_DUMP_CHARS, BoardLayout, @@ -17,6 +22,7 @@ from hopper.models.board import ( create_random_position, ) from hopper.models.player import Player, PlayerList, Position +from hopper.models.product import Product from hopper.watchdog import InactivityWatchdog from hopper.ws_server import WSServer from settings import settings @@ -82,9 +88,11 @@ class GameEngine: self.__debug_print_board() await self.ws_server.send_game_dump() - def reset_game(self) -> None: + async def reset_game(self) -> None: self.__debug_print_board() self.game_state = GameState.RUNNING + self.players.clear() + await self.send_game_dump() async def start_game_for_player(self, player_name: str) -> Player: self._start_inactivity_watchdog() @@ -99,11 +107,6 @@ class GameEngine: self.__debug_print_board() await self.send_game_dump() - - #!!!!!!!!!!!!!!! - await self._player_on_destination(player) - #!!!!!!!!!!!!!!! - await asyncio.sleep(settings.game.MOVE_DELAY) return player @@ -209,9 +212,21 @@ class GameEngine: ) self._purchase_countdown_timer.start() + async def purchase_product(self, player: Player, product: Product) -> None: + if not player.state == PlayerState.ON_DESTINATION: + raise PurchaseForbiddenForPlayer() + if self._purchase_countdown_timer: + self._purchase_countdown_timer.stop() + await self.ws_server.send_product_purchase_done_message( + player=player, product=product + ) + await self.reset_game() + def _reset_player(self, player) -> None: # move player to start position - player.position = create_player_start_position(self.board.width, self.board.height) + player.position = create_player_start_position( + self.board.width, self.board.height + ) player.state = PlayerState.CREATED player.last_seen = None diff --git a/hopper/errors.py b/hopper/errors.py index cf74c54..f4159be 100644 --- a/hopper/errors.py +++ b/hopper/errors.py @@ -12,3 +12,7 @@ class Collision(BaseError): class GameLockForMovement(BaseError): ... + + +class PurchaseForbiddenForPlayer(BaseError): + ... diff --git a/hopper/models/ws_dto.py b/hopper/models/ws_dto.py index dbc8a3d..d047b97 100644 --- a/hopper/models/ws_dto.py +++ b/hopper/models/ws_dto.py @@ -6,7 +6,14 @@ from typing import Optional, TypeVar from pydantic import Field 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, + ProductDto, +) from hopper.enums import ObjectType @@ -20,12 +27,6 @@ class LayerDto(BaseModel): objects: list[LayerObjectDto] -class ProductDto(BaseModel): - name: str - uuid: str - description: Optional[str] = None - - class GameDumpDto(BaseModel): board: BoardDto destination: DestinationDto