diff --git a/connect4-ui/app/spectate/page.tsx b/connect4-ui/app/spectate/page.tsx index 8ed4183..c5894e6 100644 --- a/connect4-ui/app/spectate/page.tsx +++ b/connect4-ui/app/spectate/page.tsx @@ -51,7 +51,6 @@ export default function SpectatePage() { const [selectedGame, setSelectedGame] = useState(null); const [log, setLog] = useState([]); - const pollRef = useRef | null>(null); const liveGamesRef = useRef>(new Map()); const addLog = useCallback( @@ -120,11 +119,48 @@ export default function SpectatePage() { break; case "TOURNAMENT_END": + setTournamentActive(false); + setTournamentType(null); addLog("Round ended"); send(cmd.gameList()); send(cmd.playerList()); break; + case "CONNECT_EVENT": + addLog(`Player joined: ${msg.username}`); + send(cmd.playerList()); + break; + + case "DISCONNECT_EVENT": + addLog(`Player left: ${msg.username}`); + send(cmd.playerList()); + send(cmd.gameList()); + break; + + case "READY_EVENT": { + let found = false; + setPlayers((prev) => + prev.map((player) => { + if (player.username !== msg.username) return player; + found = true; + return { ...player, ready: msg.ready }; + }), + ); + + if (!found) { + send(cmd.playerList()); + } + break; + } + + case "GAME_MATCH_START": + addLog( + `Match started: #${msg.matchId} ${msg.player1} vs ${msg.player2}`, + ); + send(cmd.gameList()); + send(cmd.playerList()); + break; + case "GAME_LIST": setGameList(msg.games); for (const g of msg.games) { @@ -228,29 +264,14 @@ export default function SpectatePage() { }); return unsubscribe; - }, [addLog, selectedGame, send, subscribe, updateGame]); + }, [addLog, send, subscribe, updateGame]); useEffect(() => { - if (status !== "connected" || role !== "observer") { - if (pollRef.current) { - clearInterval(pollRef.current); - pollRef.current = null; - } - return; - } + if (status !== "connected" || role !== "observer") return; send(cmd.getData("TOURNAMENT_STATUS")); send(cmd.gameList()); send(cmd.playerList()); - - pollRef.current = setInterval(() => { - send(cmd.gameList()); - send(cmd.playerList()); - }, 5000); - - return () => { - if (pollRef.current) clearInterval(pollRef.current); - }; }, [role, send, status]); const selectedGameData = diff --git a/connect4-ui/lib/protocol.ts b/connect4-ui/lib/protocol.ts index 3c7bf81..22189c4 100644 --- a/connect4-ui/lib/protocol.ts +++ b/connect4-ui/lib/protocol.ts @@ -33,11 +33,20 @@ export const RECONNECT_TIMEOUT_MS = 60000; export type ParsedMessage = | { type: "CONNECT_ACK" } + | { type: "CONNECT_EVENT"; username: string } | { type: "RECONNECT_ACK" } | { type: "DISCONNECT_ACK" } + | { type: "DISCONNECT_EVENT"; username: string } | { type: "OBSERVE_ACK"; enabled: boolean } | { type: "READY_ACK" } + | { type: "READY_EVENT"; username: string; ready: boolean } | { type: "GAME_START"; goesFirst: boolean } + | { + type: "GAME_MATCH_START"; + matchId: number; + player1: string; + player2: string; + } | { type: "GAME_WINS" } | { type: "GAME_LOSS" } | { type: "GAME_DRAW"; matchId?: number } @@ -72,12 +81,14 @@ export function parseMessage(raw: string): ParsedMessage { switch (parts[0]) { case "CONNECT": if (parts[1] === "ACK") return { type: "CONNECT_ACK" }; + return { type: "CONNECT_EVENT", username: parts[1] ?? "" }; break; case "RECONNECT": if (parts[1] === "ACK") return { type: "RECONNECT_ACK" }; break; case "DISCONNECT": if (parts[1] === "ACK") return { type: "DISCONNECT_ACK" }; + return { type: "DISCONNECT_EVENT", username: parts[1] ?? "" }; break; case "OBSERVE": if (parts[1] === "ACK") { @@ -86,6 +97,13 @@ export function parseMessage(raw: string): ParsedMessage { break; case "READY": if (parts[1] === "ACK") return { type: "READY_ACK" }; + if (parts.length >= 3) { + return { + type: "READY_EVENT", + username: parts[1], + ready: parts[2] === "true", + }; + } break; case "GAME": { @@ -114,6 +132,15 @@ export function parseMessage(raw: string): ParsedMessage { switch (parts[1]) { case "START": + if (parts[2]?.includes(",")) { + const [matchId, player1, player2] = parts[2].split(","); + return { + type: "GAME_MATCH_START", + matchId: parseInt(matchId, 10), + player1, + player2, + }; + } return { type: "GAME_START", goesFirst: parts[2] === "1" }; case "WINS": return { type: "GAME_WINS" };