import { useEffect, useRef, useState, useCallback } from 'react'; import type { AvailableMovesResponse, BoardState, MoveResponse, Player, TurnInfo } from '../types'; const INITIAL_BOARD: BoardState = { config: { min_x: 0, max_x: 64, min_y: 0, max_y: 64, grid_cells_x: 64, grid_cells_y: 64, }, player_count: 0, players: [], turn: { current_player_id: null, current_player_name: null, round_number: 1, turn_number: 0, turn_order: [], }, }; export function useGameSocket() { const [boardState, setBoardState] = useState(INITIAL_BOARD); const [isConnected, setIsConnected] = useState(false); const [selectedPlayer, setSelectedPlayer] = useState(null); const [availableMoves, setAvailableMoves] = useState(null); const [isAutoPlaying, setIsAutoPlaying] = useState(false); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const autoPlayIntervalRef = useRef(null); const fetchBoard = useCallback(async () => { try { const res = await fetch('/api/board'); if (res.ok) { const data: BoardState = await res.json(); setBoardState(data); } } catch (err) { console.error('Failed to fetch board state:', err); } }, []); const fetchAvailableMoves = useCallback(async (playerId: string) => { try { const res = await fetch(`/api/players/${playerId}/available-moves`); if (res.ok) { const data: AvailableMovesResponse = await res.json(); setAvailableMoves(data); return data; } } catch (err) { console.error('Failed to fetch available moves:', err); } return null; }, []); const connectWebSocket = useCallback(() => { if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) { return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; const wsUrl = `${protocol}//${host}/ws`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { setIsConnected(true); console.log('Connected to botWebWars WebSocket'); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.event === 'init') { setBoardState(data.state); } else if (data.event === 'player_joined') { setBoardState((prev) => { const exists = prev.players.some((p) => p.id === data.player.id); const nextPlayers = exists ? prev.players.map((p) => (p.id === data.player.id ? data.player : p)) : [...prev.players, data.player]; return { ...prev, player_count: data.player_count ?? nextPlayers.length, players: nextPlayers, turn: data.turn ?? prev.turn, }; }); } else if (data.event === 'player_moved') { setBoardState((prev) => ({ ...prev, players: prev.players.map((p) => (p.id === data.player.id ? data.player : p)), turn: data.turn ?? prev.turn, })); } else if (data.event === 'turn_passed') { setBoardState((prev) => ({ ...prev, turn: data.turn ?? prev.turn, })); } else if (data.event === 'player_left') { setBoardState((prev) => { const nextPlayers = prev.players.filter((p) => p.id !== data.player_id); return { ...prev, player_count: data.player_count ?? nextPlayers.length, players: nextPlayers, turn: data.turn ?? prev.turn, }; }); setSelectedPlayer((prev) => (prev?.id === data.player_id ? null : prev)); } else if (data.event === 'board_reset') { setBoardState((prev) => ({ ...prev, player_count: 0, players: [], turn: data.turn ?? INITIAL_BOARD.turn, })); setSelectedPlayer(null); setAvailableMoves(null); } } catch (err) { console.error('Error parsing WebSocket message:', err); } }; ws.onclose = () => { setIsConnected(false); reconnectTimeoutRef.current = window.setTimeout(() => { connectWebSocket(); }, 2500); }; ws.onerror = (err) => { console.error('WebSocket encountered an error:', err); ws.close(); }; }, []); useEffect(() => { fetchBoard(); connectWebSocket(); return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); } }; }, [fetchBoard, connectWebSocket]); // Keep selected player synced with board state useEffect(() => { if (selectedPlayer) { const updated = boardState.players.find((p) => p.id === selectedPlayer.id); if (updated) { setSelectedPlayer(updated); } else { setSelectedPlayer(null); setAvailableMoves(null); } } }, [boardState.players, selectedPlayer]); // Auto-fetch available moves for active player or selected player useEffect(() => { const targetId = selectedPlayer?.id || boardState.turn.current_player_id; if (targetId) { fetchAvailableMoves(targetId); } else { setAvailableMoves(null); } }, [selectedPlayer?.id, boardState.turn.current_player_id, boardState.players, fetchAvailableMoves]); const registerPlayer = async (name: string, color: string): Promise => { const res = await fetch('/api/players', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, color }), }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.detail || 'Failed to register player'); } const newPlayer: Player = await res.json(); setSelectedPlayer(newPlayer); return newPlayer; }; const removePlayer = async (id: string) => { const res = await fetch(`/api/players/${id}`, { method: 'DELETE', }); if (!res.ok) { throw new Error('Failed to remove player'); } }; const movePlayer = async (playerId: string, direction: string): Promise => { const res = await fetch(`/api/players/${playerId}/move`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || 'Failed to move player'); } const data: MoveResponse = await res.json(); return data; }; const passTurn = async (playerId: string): Promise => { const res = await fetch(`/api/players/${playerId}/pass`, { method: 'POST', }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || 'Failed to pass turn'); } const data: TurnInfo = await res.json(); return data; }; const resetBoard = async () => { const res = await fetch('/api/board/reset', { method: 'POST', }); if (!res.ok) { throw new Error('Failed to reset board'); } }; // Step a single turn for the active bot (random valid move) const stepActiveBotTurn = useCallback(async () => { const currentId = boardState.turn.current_player_id; if (!currentId) return; try { const movesData = await fetchAvailableMoves(currentId); if (!movesData) return; const validDirections = Object.entries(movesData.moves) .filter(([, result]) => result.available) .map(([dir]) => dir); if (validDirections.length > 0) { const randomDir = validDirections[Math.floor(Math.random() * validDirections.length)]; await movePlayer(currentId, randomDir); } else { // No moves available, pass turn await passTurn(currentId); } } catch (err) { console.warn('Bot turn step error:', err); } }, [boardState.turn.current_player_id, fetchAvailableMoves]); // Autoplay interval loop useEffect(() => { if (isAutoPlaying) { autoPlayIntervalRef.current = window.setInterval(() => { stepActiveBotTurn(); }, 700); } else if (autoPlayIntervalRef.current) { clearInterval(autoPlayIntervalRef.current); autoPlayIntervalRef.current = null; } return () => { if (autoPlayIntervalRef.current) { clearInterval(autoPlayIntervalRef.current); } }; }, [isAutoPlaying, stepActiveBotTurn]); return { boardState, isConnected, selectedPlayer, setSelectedPlayer, availableMoves, isAutoPlaying, setIsAutoPlaying, registerPlayer, removePlayer, movePlayer, passTurn, stepActiveBotTurn, resetBoard, refresh: fetchBoard, }; }