botWebWars/backend/app/api/routes.py

488 lines
14 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query, status
from app.api.websocket import manager
from app.game import game_engine
from app.models import (
ApiResponse,
AvailableMovesResponse,
BattleRequest,
BattleResult,
BoardState,
BotMemoryResponse,
BotRadarResponse,
MoveCheckResult,
MoveRequest,
MoveResponse,
Party,
PartyDefeatResult,
PartyDirectForm,
PartyInvite,
PartyInviteCreate,
PartyInviteResponse,
Player,
PlayerCreate,
TurnInfo,
)
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"},
)
# ==========================================
# Player Registration & Management
# ==========================================
@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)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "player_joined",
"player": player.model_dump(),
"player_count": board_state.player_count,
"turn": board_state.turn.model_dump(),
})
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",
)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "player_left",
"player_id": player_id,
"player_count": board_state.player_count,
"parties": [p.model_dump() for p in board_state.parties],
"turn": board_state.turn.model_dump(),
})
return ApiResponse(
success=True,
message=f"Player '{player_id}' successfully removed",
)
# ==========================================
# Bot Memory & Radar Endpoints
# ==========================================
@router.get(
"/players/{player_id}/memory",
response_model=BotMemoryResponse,
summary="Retrieve a bot's current location and visited locations memory",
tags=["Bot Intelligence"],
)
async def get_bot_memory(
player_id: str,
check_x: Optional[int] = Query(None, description="Check if bot has visited this X coordinate"),
check_y: Optional[int] = Query(None, description="Check if bot has visited this Y coordinate"),
):
try:
return await game_engine.get_bot_memory(player_id, check_x, check_y)
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Player '{player_id}' not found",
)
@router.get(
"/players/{player_id}/radar",
response_model=BotRadarResponse,
summary="Scan surroundings for other bots, distance, allies/enemies, and recommended direction",
tags=["Bot Intelligence"],
)
async def get_bot_radar(player_id: str):
try:
return await game_engine.get_bot_radar(player_id)
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Player '{player_id}' not found",
)
# ==========================================
# Party & Squad Endpoints
# ==========================================
@router.get(
"/parties",
response_model=List[Party],
summary="List all active parties",
tags=["Parties"],
)
async def list_parties():
return await game_engine.get_all_parties()
@router.get(
"/parties/{party_id}",
response_model=Party,
summary="Get details of a specific party",
tags=["Parties"],
)
async def get_party(party_id: str):
party = await game_engine.get_party(party_id)
if not party:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Party '{party_id}' not found",
)
return party
@router.post(
"/parties",
response_model=Party,
status_code=status.HTTP_201_CREATED,
summary="Directly form a party with adjacent linked members and an agreed leader",
tags=["Parties"],
)
async def form_party(form_req: PartyDirectForm):
try:
party = await game_engine.form_party(
member_ids=form_req.member_ids,
leader_id=form_req.leader_id,
name=form_req.name,
)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "party_formed",
"party": party.model_dump(),
"players": [p.model_dump() for p in board_state.players],
"turn": board_state.turn.model_dump(),
})
return party
except KeyError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/parties/invite",
response_model=PartyInvite,
summary="Invite an adjacent bot to join/form a party with an agreed leader",
tags=["Parties"],
)
async def invite_to_party(invite_req: PartyInviteCreate):
try:
invite = await game_engine.invite_to_party(
inviter_id=invite_req.inviter_id,
invitee_id=invite_req.invitee_id,
proposed_leader_id=invite_req.proposed_leader_id,
party_name=invite_req.party_name,
)
return invite
except KeyError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/parties/invites/{invite_id}/respond",
response_model=Optional[Party],
summary="Accept or reject a party invitation",
tags=["Parties"],
)
async def respond_to_invite(invite_id: str, resp: PartyInviteResponse):
try:
party = await game_engine.respond_to_invite(invite_id, resp.accept)
if party:
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "party_updated",
"party": party.model_dump(),
"players": [p.model_dump() for p in board_state.players],
"turn": board_state.turn.model_dump(),
})
return party
except KeyError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/parties/{party_id}/defeat",
response_model=PartyDefeatResult,
summary="Trigger defeat for a party: leader killed, respawns, score -1, remainder assigns new leader",
tags=["Parties"],
)
async def defeat_party(party_id: str):
try:
result = await game_engine.defeat_party(party_id)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "party_defeated",
"defeat_details": result.model_dump(),
"players": [p.model_dump() for p in board_state.players],
"parties": [p.model_dump() for p in board_state.parties],
"turn": board_state.turn.model_dump(),
})
return result
except KeyError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
@router.post(
"/battles/fight",
response_model=BattleResult,
summary="Initiate a 3-bout D20 battle between two adjacent parties/bots",
tags=["Battles"],
)
async def fight_battle(battle_req: BattleRequest):
try:
result = await game_engine.battle(battle_req.challenger_id, battle_req.defender_id)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "battle_resolved",
"battle": result.model_dump(),
"players": [p.model_dump() for p in board_state.players],
"parties": [p.model_dump() for p in board_state.parties],
"turn": board_state.turn.model_dump(),
})
return result
except KeyError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# ==========================================
# Movement & Turn Endpoints
# ==========================================
@router.get(
"/players/{player_id}/available-moves",
response_model=AvailableMovesResponse,
summary="Check availability of all 8 movement directions for a bot or their party",
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",
),
):
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 (or entire party if leader) in a cardinal or diagonal direction during 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)
board_state = await game_engine.get_board_state()
await manager.broadcast({
"event": "player_moved",
"player": result.player.model_dump(),
"direction": result.direction,
"party_moved": result.party_moved,
"affected_players": [p.model_dump() for p in result.affected_players],
"previous_position": result.previous_position,
"new_position": result.new_position,
"players": [p.model_dump() for p in board_state.players],
"turn": result.turn.model_dump(),
})
if result.battle_triggered and result.battle_result:
await manager.broadcast({
"event": "battle_resolved",
"battle": result.battle_result.model_dump(),
"players": [p.model_dump() for p in board_state.players],
"parties": [p.model_dump() for p in board_state.parties],
"turn": board_state.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
# ==========================================
@router.get(
"/board",
response_model=BoardState,
summary="Get full board configuration, parties, 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 and parties",
tags=["Board"],
)
async def reset_board():
await game_engine.reset()
board = await game_engine.get_board_state()
await manager.broadcast({
"event": "board_reset",
"player_count": 0,
"turn": board.turn.model_dump(),
})
return ApiResponse(
success=True,
message="Board reset successfully. All players cleared.",
)