from enum import Enum from typing import List, Optional import requests from pydantic import BaseModel class BaseError(Exception): detail: str = "BaseError" def __str__(self) -> str: return self.detail class PositionError(BaseError): detail = "Invalid position" class PlayerInactiveError(BaseError): detail = "Player inactive" class PlayerNotFoundError(BaseError): detail = "Player not found" class GameLockedError(BaseError): detail = "Game locked. Product selection in progress." class ValidationError(BaseError): detail = "Validation error" 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 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 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) if r.status_code == 422: raise ValidationError() else: 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, player_id: str) -> PlayerInfoResponse: r = requests.get(self.format_url(f"/player/{player_id}")) if r.status_code == 403: raise PlayerInactiveError() elif r.status_code == 404: raise PlayerNotFoundError() elif r.status_code == 422: raise ValidationError() 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() elif r.status_code == 422: raise ValidationError() elif r.status_code == 423: raise GameLockedError() else: r.raise_for_status() return PlayerInfoResponse(**r.json())