Product purchase
This commit is contained in:
@ -34,6 +34,15 @@ POST http://localhost:8010/player/test-player-pero/move/up
|
|||||||
POST http://localhost:8010/player/test-player-pero/move/down
|
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
|
# move Mirko left
|
||||||
POST http://localhost:8010/player/test-player-mirko/move/left
|
POST http://localhost:8010/player/test-player-mirko/move/left
|
||||||
###
|
###
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel as PydanticBaseModel
|
from pydantic import BaseModel as PydanticBaseModel
|
||||||
|
|
||||||
from hopper.enums import PlayerState
|
from hopper.enums import PlayerState
|
||||||
@ -38,6 +40,11 @@ class DestinationDto(BaseModel):
|
|||||||
position: PositionDto
|
position: PositionDto
|
||||||
|
|
||||||
|
|
||||||
|
class ProductDto(BaseModel):
|
||||||
|
name: str
|
||||||
|
uuid: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
class StartGameRequestDto(BaseModel):
|
class StartGameRequestDto(BaseModel):
|
||||||
player_name: str
|
player_name: str
|
||||||
|
|
||||||
@ -61,3 +68,11 @@ class PlayerInfoResponseDto(MovePlayerResponseDto):
|
|||||||
|
|
||||||
class ErrorResponseDto(BaseModel):
|
class ErrorResponseDto(BaseModel):
|
||||||
detail: str
|
detail: str
|
||||||
|
|
||||||
|
|
||||||
|
class GetProductsResponse(BaseModel):
|
||||||
|
products: list[ProductDto]
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseProductDto(BaseModel):
|
||||||
|
product_uuid: str
|
||||||
|
|||||||
@ -6,16 +6,20 @@ from hopper.api.dto import (
|
|||||||
DestinationDto,
|
DestinationDto,
|
||||||
ErrorResponseDto,
|
ErrorResponseDto,
|
||||||
GameInfoDto,
|
GameInfoDto,
|
||||||
|
GetProductsResponse,
|
||||||
MovePlayerResponseDto,
|
MovePlayerResponseDto,
|
||||||
PingResponse,
|
PingResponse,
|
||||||
PlayerInfoResponseDto,
|
PlayerInfoResponseDto,
|
||||||
|
ProductDto,
|
||||||
|
PurchaseProductDto,
|
||||||
StartGameRequestDto,
|
StartGameRequestDto,
|
||||||
StartGameResponseDto,
|
StartGameResponseDto,
|
||||||
)
|
)
|
||||||
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, GameLockForMovement, PositionOutOfBounds
|
from hopper.errors import Collision, GameLockForMovement, PositionOutOfBounds, PurchaseForbiddenForPlayer
|
||||||
from hopper.models.player import Player
|
from hopper.models.player import Player
|
||||||
|
from settings import settings
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -144,3 +148,42 @@ async def move_player(
|
|||||||
response.status_code = status.HTTP_200_OK
|
response.status_code = status.HTTP_200_OK
|
||||||
|
|
||||||
return MovePlayerResponseDto(player=player)
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@ -5,7 +5,10 @@ from typing import Callable, Optional
|
|||||||
|
|
||||||
class CountdownTimer(Thread):
|
class CountdownTimer(Thread):
|
||||||
def __init__(
|
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:
|
) -> None:
|
||||||
self.seconds = seconds
|
self.seconds = seconds
|
||||||
self.stop_event = Event()
|
self.stop_event = Event()
|
||||||
@ -18,7 +21,7 @@ class CountdownTimer(Thread):
|
|||||||
while time_left and not self.stop_event.is_set():
|
while time_left and not self.stop_event.is_set():
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
time_left -= 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)
|
self.timer_tick_callback(time_left)
|
||||||
|
|
||||||
if time_left == 0 and self.timer_done_callback:
|
if time_left == 0 and self.timer_done_callback:
|
||||||
|
|||||||
@ -5,7 +5,12 @@ from typing import Optional
|
|||||||
|
|
||||||
from hopper.countdown_timer import CountdownTimer
|
from hopper.countdown_timer import CountdownTimer
|
||||||
from hopper.enums import Direction, GameState, PlayerMoveResult, PlayerState
|
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 (
|
from hopper.models.board import (
|
||||||
BOARD_DUMP_CHARS,
|
BOARD_DUMP_CHARS,
|
||||||
BoardLayout,
|
BoardLayout,
|
||||||
@ -17,6 +22,7 @@ from hopper.models.board import (
|
|||||||
create_random_position,
|
create_random_position,
|
||||||
)
|
)
|
||||||
from hopper.models.player import Player, PlayerList, Position
|
from hopper.models.player import Player, PlayerList, Position
|
||||||
|
from hopper.models.product import Product
|
||||||
from hopper.watchdog import InactivityWatchdog
|
from hopper.watchdog import InactivityWatchdog
|
||||||
from hopper.ws_server import WSServer
|
from hopper.ws_server import WSServer
|
||||||
from settings import settings
|
from settings import settings
|
||||||
@ -82,9 +88,11 @@ class GameEngine:
|
|||||||
self.__debug_print_board()
|
self.__debug_print_board()
|
||||||
await self.ws_server.send_game_dump()
|
await self.ws_server.send_game_dump()
|
||||||
|
|
||||||
def reset_game(self) -> None:
|
async def reset_game(self) -> None:
|
||||||
self.__debug_print_board()
|
self.__debug_print_board()
|
||||||
self.game_state = GameState.RUNNING
|
self.game_state = GameState.RUNNING
|
||||||
|
self.players.clear()
|
||||||
|
await self.send_game_dump()
|
||||||
|
|
||||||
async def start_game_for_player(self, player_name: str) -> Player:
|
async def start_game_for_player(self, player_name: str) -> Player:
|
||||||
self._start_inactivity_watchdog()
|
self._start_inactivity_watchdog()
|
||||||
@ -99,11 +107,6 @@ class GameEngine:
|
|||||||
self.__debug_print_board()
|
self.__debug_print_board()
|
||||||
|
|
||||||
await self.send_game_dump()
|
await self.send_game_dump()
|
||||||
|
|
||||||
#!!!!!!!!!!!!!!!
|
|
||||||
await self._player_on_destination(player)
|
|
||||||
#!!!!!!!!!!!!!!!
|
|
||||||
|
|
||||||
await asyncio.sleep(settings.game.MOVE_DELAY)
|
await asyncio.sleep(settings.game.MOVE_DELAY)
|
||||||
return player
|
return player
|
||||||
|
|
||||||
@ -209,9 +212,21 @@ class GameEngine:
|
|||||||
)
|
)
|
||||||
self._purchase_countdown_timer.start()
|
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:
|
def _reset_player(self, player) -> None:
|
||||||
# move player to start position
|
# 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.state = PlayerState.CREATED
|
||||||
player.last_seen = None
|
player.last_seen = None
|
||||||
|
|
||||||
|
|||||||
@ -12,3 +12,7 @@ class Collision(BaseError):
|
|||||||
|
|
||||||
class GameLockForMovement(BaseError):
|
class GameLockForMovement(BaseError):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseForbiddenForPlayer(BaseError):
|
||||||
|
...
|
||||||
|
|||||||
@ -6,7 +6,14 @@ from typing import Optional, TypeVar
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic.generics import GenericModel
|
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
|
from hopper.enums import ObjectType
|
||||||
|
|
||||||
|
|
||||||
@ -20,12 +27,6 @@ class LayerDto(BaseModel):
|
|||||||
objects: list[LayerObjectDto]
|
objects: list[LayerObjectDto]
|
||||||
|
|
||||||
|
|
||||||
class ProductDto(BaseModel):
|
|
||||||
name: str
|
|
||||||
uuid: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class GameDumpDto(BaseModel):
|
class GameDumpDto(BaseModel):
|
||||||
board: BoardDto
|
board: BoardDto
|
||||||
destination: DestinationDto
|
destination: DestinationDto
|
||||||
|
|||||||
Reference in New Issue
Block a user