from typing import Optional from pydantic import BaseModel from enum import Enum import requests class BaseError(Exception): ... class PositionError(BaseError): ... class PlayerInactiveError(BaseError): ... class PlayerNotFoundError(BaseError): ... class Direction(str, Enum): LEFT = "left" RIGHT = "right" UP = "up" DOWN = "down" class PlayerState(str, Enum): CREATED = "CREATED" MOVING = "MOVING" ON_DESTINATION = "ON_DESTINATION" INACTIVE = "INACTIVE" class Board(BaseModel): width: int height: int class Position(BaseModel): x: int y: int class Destination(BaseModel): position: Position class Player(BaseModel): id: str name: str active: bool position: Position move_count: int move_attempt_count: int state: PlayerState class Product(BaseModel): name: str id: str description: Optional[str] = None class PingResponse(BaseModel): message: str class StartGameResponse(BaseModel): board: Board destination: Destination player: Player class PlayerInfoResponse(BaseModel): player: Player class GameInfoResponse(BaseModel): board: Board destination: Destination class ProductListResponse(BaseModel): products: list[Product] class FairHopper: def __init__(self, host: str) -> None: self.host = host def format_url(self, path: str) -> str: return f"{self.host}{path}" def ping(self) -> PingResponse: r = requests.get(self.format_url("/ping")) r.raise_for_status() return PingResponse(**r.json()) def start_game(self, player_name: str) -> StartGameResponse: payload = { "player_name": player_name, } r = requests.post(self.format_url("/game"), json=payload) r.raise_for_status() return StartGameResponse(**r.json()) def get_game_info(self) -> GameInfoResponse: r = requests.get(self.format_url(f"/game")) r.raise_for_status() return GameInfoResponse(**r.json()) def get_player_info(self, id: str) -> PlayerInfoResponse: r = requests.get(self.format_url(f"/player/{id}")) if r.status_code == 403: raise PlayerInactiveError() elif r.status_code == 404: raise PlayerNotFoundError() else: r.raise_for_status() return PlayerInfoResponse(**r.json()) def move_left(self, player_id: str) -> PlayerInfoResponse: return self.move(player_id, Direction.LEFT) def move_right(self, player_id: str) -> PlayerInfoResponse: return self.move(player_id, Direction.RIGHT) def move_up(self, player_id: str) -> PlayerInfoResponse: return self.move(player_id, Direction.UP) def move_down(self, player_id: str) -> PlayerInfoResponse: return self.move(player_id, Direction.DOWN) def move(self, player_id: str, direction: Direction) -> PlayerInfoResponse: path = f"/player/{player_id}/move/{direction}" r = requests.post(self.format_url(path)) if r.status_code == 403: raise PlayerInactiveError() elif r.status_code == 404: raise PlayerNotFoundError() elif r.status_code == 409: raise PositionError() else: r.raise_for_status() return PlayerInfoResponse(**r.json()) def get_products(self) -> list[Product]: r = requests.get(self.format_url("/products")) response_data = ProductListResponse(**r.json()) return response_data.products def purchase_product(self, player_id: str, product_id: str) -> Product: url = self.format_url(f"/player/{player_id}/product/purchase") payload = { "product_id": product_id, } r = requests.post(url, json=payload) r.raise_for_status() return Product(**r.json())