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.", )