2026-09-05 21:20:39 +00:00
|
|
|
from typing import List, Optional
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
2026-09-05 21:10:18 +00:00
|
|
|
from app.api.websocket import manager
|
|
|
|
|
from app.game import game_engine
|
2026-09-05 21:20:39 +00:00
|
|
|
from app.models import (
|
|
|
|
|
ApiResponse,
|
|
|
|
|
AvailableMovesResponse,
|
|
|
|
|
BoardState,
|
|
|
|
|
MoveCheckResult,
|
|
|
|
|
MoveRequest,
|
|
|
|
|
MoveResponse,
|
|
|
|
|
Player,
|
|
|
|
|
PlayerCreate,
|
|
|
|
|
TurnInfo,
|
|
|
|
|
)
|
2026-09-05 21:10:18 +00:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/health", response_model=ApiResponse, tags=["System"])
|
|
|
|
|
async def health_check():
|
|
|
|
|
return ApiResponse(
|
|
|
|
|
success=True,
|
|
|
|
|
message="botWebWars backend service is healthy",
|
|
|
|
|
data={"service": "FastAPI", "status": "running"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/players",
|
|
|
|
|
response_model=Player,
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
summary="Register a new player on the board",
|
|
|
|
|
tags=["Players"],
|
|
|
|
|
)
|
|
|
|
|
@router.post(
|
|
|
|
|
"/register",
|
|
|
|
|
response_model=Player,
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
include_in_schema=False,
|
|
|
|
|
)
|
|
|
|
|
async def register_player(player_in: PlayerCreate):
|
|
|
|
|
player = await game_engine.register_player(player_in)
|
2026-09-05 21:20:39 +00:00
|
|
|
board_state = await game_engine.get_board_state()
|
2026-09-05 21:10:18 +00:00
|
|
|
|
|
|
|
|
# Broadcast to all connected WebSocket clients
|
|
|
|
|
await manager.broadcast({
|
|
|
|
|
"event": "player_joined",
|
|
|
|
|
"player": player.model_dump(),
|
2026-09-05 21:20:39 +00:00
|
|
|
"player_count": board_state.player_count,
|
|
|
|
|
"turn": board_state.turn.model_dump(),
|
2026-09-05 21:10:18 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return player
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/players",
|
|
|
|
|
response_model=List[Player],
|
|
|
|
|
summary="List all registered players",
|
|
|
|
|
tags=["Players"],
|
|
|
|
|
)
|
|
|
|
|
async def list_players():
|
|
|
|
|
return await game_engine.get_all_players()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/players/{player_id}",
|
|
|
|
|
response_model=Player,
|
|
|
|
|
summary="Get details of a specific player",
|
|
|
|
|
tags=["Players"],
|
|
|
|
|
)
|
|
|
|
|
async def get_player(player_id: str):
|
|
|
|
|
player = await game_engine.get_player(player_id)
|
|
|
|
|
if not player:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
return player
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/players/{player_id}",
|
|
|
|
|
response_model=ApiResponse,
|
|
|
|
|
summary="Remove a player from the board",
|
|
|
|
|
tags=["Players"],
|
|
|
|
|
)
|
|
|
|
|
async def remove_player(player_id: str):
|
|
|
|
|
removed = await game_engine.remove_player(player_id)
|
|
|
|
|
if not removed:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-05 21:20:39 +00:00
|
|
|
board_state = await game_engine.get_board_state()
|
2026-09-05 21:10:18 +00:00
|
|
|
await manager.broadcast({
|
|
|
|
|
"event": "player_left",
|
|
|
|
|
"player_id": player_id,
|
2026-09-05 21:20:39 +00:00
|
|
|
"player_count": board_state.player_count,
|
|
|
|
|
"turn": board_state.turn.model_dump(),
|
2026-09-05 21:10:18 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return ApiResponse(
|
|
|
|
|
success=True,
|
|
|
|
|
message=f"Player '{player_id}' successfully removed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-09-05 21:20:39 +00:00
|
|
|
# ==========================================
|
|
|
|
|
# Movement & Turn Endpoints
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/players/{player_id}/available-moves",
|
|
|
|
|
response_model=AvailableMovesResponse,
|
|
|
|
|
summary="Check availability of all 8 movement directions (cardinal & diagonal)",
|
|
|
|
|
tags=["Movement"],
|
|
|
|
|
)
|
|
|
|
|
async def get_available_moves(player_id: str):
|
|
|
|
|
try:
|
|
|
|
|
return await game_engine.get_available_moves(player_id)
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/players/{player_id}/check-move",
|
|
|
|
|
response_model=MoveCheckResult,
|
|
|
|
|
summary="Check if a specific direction is available for movement",
|
|
|
|
|
tags=["Movement"],
|
|
|
|
|
)
|
|
|
|
|
async def check_single_move(
|
|
|
|
|
player_id: str,
|
|
|
|
|
direction: str = Query(
|
|
|
|
|
...,
|
|
|
|
|
description="Direction to check: UP, DOWN, LEFT, RIGHT, UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT (or N, S, E, W, NW, NE, SW, SE)",
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
try:
|
|
|
|
|
return await game_engine.check_single_move(player_id, direction)
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail=str(e),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/players/{player_id}/move",
|
|
|
|
|
response_model=MoveResponse,
|
|
|
|
|
summary="Move player in a cardinal or diagonal direction during their turn",
|
|
|
|
|
tags=["Movement"],
|
|
|
|
|
)
|
|
|
|
|
async def move_player(player_id: str, move_req: MoveRequest):
|
|
|
|
|
try:
|
|
|
|
|
dx, dy, dir_name = move_req.get_delta()
|
|
|
|
|
result = await game_engine.move_player(player_id, dx, dy, dir_name)
|
|
|
|
|
|
|
|
|
|
# Broadcast move event to all connected WebSocket clients
|
|
|
|
|
await manager.broadcast({
|
|
|
|
|
"event": "player_moved",
|
|
|
|
|
"player": result.player.model_dump(),
|
|
|
|
|
"direction": result.direction,
|
|
|
|
|
"previous_position": result.previous_position,
|
|
|
|
|
"new_position": result.new_position,
|
|
|
|
|
"turn": result.turn.model_dump(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail=str(e),
|
|
|
|
|
)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail=str(e),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/players/{player_id}/pass",
|
|
|
|
|
response_model=TurnInfo,
|
|
|
|
|
summary="Pass the current turn to the next player",
|
|
|
|
|
tags=["Movement"],
|
|
|
|
|
)
|
|
|
|
|
async def pass_turn(player_id: str):
|
|
|
|
|
try:
|
|
|
|
|
next_turn = await game_engine.pass_turn(player_id)
|
|
|
|
|
await manager.broadcast({
|
|
|
|
|
"event": "turn_passed",
|
|
|
|
|
"passed_by": player_id,
|
|
|
|
|
"turn": next_turn.model_dump(),
|
|
|
|
|
})
|
|
|
|
|
return next_turn
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
detail=f"Player '{player_id}' not found",
|
|
|
|
|
)
|
|
|
|
|
except PermissionError as e:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
detail=str(e),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/turn",
|
|
|
|
|
response_model=TurnInfo,
|
|
|
|
|
summary="Get current turn details and round information",
|
|
|
|
|
tags=["Turn"],
|
|
|
|
|
)
|
|
|
|
|
async def get_turn():
|
|
|
|
|
board = await game_engine.get_board_state()
|
|
|
|
|
return board.turn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
# Board Endpoints
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
2026-09-05 21:10:18 +00:00
|
|
|
@router.get(
|
|
|
|
|
"/board",
|
|
|
|
|
response_model=BoardState,
|
|
|
|
|
summary="Get full board configuration and player positions",
|
|
|
|
|
tags=["Board"],
|
|
|
|
|
)
|
|
|
|
|
async def get_board():
|
|
|
|
|
return await game_engine.get_board_state()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/board/reset",
|
|
|
|
|
response_model=ApiResponse,
|
|
|
|
|
summary="Reset the board and clear all players",
|
|
|
|
|
tags=["Board"],
|
|
|
|
|
)
|
|
|
|
|
async def reset_board():
|
|
|
|
|
await game_engine.reset()
|
2026-09-05 21:20:39 +00:00
|
|
|
board = await game_engine.get_board_state()
|
2026-09-05 21:10:18 +00:00
|
|
|
await manager.broadcast({
|
|
|
|
|
"event": "board_reset",
|
|
|
|
|
"player_count": 0,
|
2026-09-05 21:20:39 +00:00
|
|
|
"turn": board.turn.model_dump(),
|
2026-09-05 21:10:18 +00:00
|
|
|
})
|
|
|
|
|
return ApiResponse(
|
|
|
|
|
success=True,
|
|
|
|
|
message="Board reset successfully. All players cleared.",
|
|
|
|
|
)
|