"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Confetti from "react-confetti"; import Board from "@/components/Board"; import { BoardState, ParsedMessage, cmd, createEmptyBoard, placeToken, } from "@/lib/protocol"; import { useConnection } from "@/lib/connection"; type GamePhase = "idle" | "connected" | "ready" | "playing" | "game-over"; type GameResult = "win" | "loss" | "draw" | "terminated"; export default function PlayPage() { const router = useRouter(); const { role, username, status, send, subscribe, reconnectAttempts, shouldRedirectToConnect, clearRedirectFlag, } = useConnection(); const [gamePhase, setGamePhase] = useState("idle"); const [myColor, setMyColor] = useState<1 | 2 | null>(null); const [isMyTurn, setIsMyTurn] = useState(false); const [board, setBoard] = useState(createEmptyBoard()); const [lastMove, setLastMove] = useState<{ column: number; row: number; } | null>(null); const [gameResult, setGameResult] = useState(null); const [tournamentMode, setTournamentMode] = useState(false); const [showWinConfetti, setShowWinConfetti] = useState(false); const [winConfettiBurstId, setWinConfettiBurstId] = useState(0); const [viewport, setViewport] = useState({ width: 0, height: 0 }); const myColorRef = useRef<1 | 2 | null>(null); const isMyTurnRef = useRef(false); const resetGame = useCallback(() => { setBoard(createEmptyBoard()); setLastMove(null); setMyColor(null); myColorRef.current = null; setIsMyTurn(false); isMyTurnRef.current = false; setGameResult(null); }, []); useEffect(() => { const updateViewport = () => { setViewport({ width: window.innerWidth, height: window.innerHeight, }); }; updateViewport(); window.addEventListener("resize", updateViewport); return () => window.removeEventListener("resize", updateViewport); }, []); useEffect(() => { if (status === "disconnected" && shouldRedirectToConnect) { clearRedirectFlag(); router.replace("/"); } if (status === "idle") { router.replace("/"); } if (role !== "player" && status !== "idle") { router.replace("/spectate"); return; } if (status === "connected" && gamePhase === "idle") { setGamePhase("connected"); } }, [ role, status, router, gamePhase, shouldRedirectToConnect, clearRedirectFlag, ]); useEffect(() => { const unsubscribe = subscribe((msg: ParsedMessage) => { switch (msg.type) { case "CONNECT_ACK": case "RECONNECT_ACK": setGamePhase((prev) => (prev === "idle" ? "connected" : prev)); break; case "ERROR": break; case "READY_ACK": setGamePhase("ready"); break; case "GAME_START": { resetGame(); const color: 1 | 2 = msg.goesFirst ? 1 : 2; setMyColor(color); myColorRef.current = color; setGamePhase("playing"); const firstTurn = msg.goesFirst; setIsMyTurn(firstTurn); isMyTurnRef.current = firstTurn; break; } case "OPPONENT_MOVE": { const opponentColor: 1 | 2 = myColorRef.current === 1 ? 2 : 1; setBoard((prev) => { const { board: next, row } = placeToken( prev, opponentColor, msg.column, ); setLastMove({ column: msg.column, row }); return next; }); setIsMyTurn(true); isMyTurnRef.current = true; break; } case "GAME_WINS": setGameResult("win"); setWinConfettiBurstId((prev) => prev + 1); setShowWinConfetti(true); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; break; case "GAME_LOSS": setGameResult("loss"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; break; case "GAME_DRAW": setGameResult("draw"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; break; case "GAME_TERMINATED": setGameResult("terminated"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; break; case "TOURNAMENT_START": setTournamentMode(true); break; case "TOURNAMENT_END": setGamePhase("connected"); resetGame(); send(cmd.ready()); setGamePhase("ready"); break; case "TOURNAMENT_CANCEL": setTournamentMode(false); setGamePhase("connected"); resetGame(); break; default: break; } }); return unsubscribe; }, [resetGame, send, subscribe]); const handleColumnClick = useCallback( (col: number) => { if (!isMyTurnRef.current || gamePhase !== "playing") return; const color = myColorRef.current; if (!color) return; setBoard((prev) => { const { board: next, row } = placeToken(prev, color, col); if (row === -1) return prev; setLastMove({ column: col, row }); return next; }); setIsMyTurn(false); isMyTurnRef.current = false; send(cmd.play(col)); }, [gamePhase, send], ); const sendReady = useCallback(() => { send(cmd.ready()); setGamePhase("ready"); }, [send]); const myColorLabel = myColor === 1 ? "🔴 Red" : myColor === 2 ? "🟡 Yellow" : null; const opponentColor: 1 | 2 | null = myColor === 1 ? 2 : myColor === 2 ? 1 : null; const redPlayerName = myColor === 1 ? username : "Opponent"; const yellowPlayerName = myColor === 2 ? username : "Opponent"; return (
{showWinConfetti && ( setShowWinConfetti(false)} numberOfPieces={300} gravity={0.28} className="pointer-events-none fixed! inset-0! z-40!" /> )}

🎮 Play Connect4

Connected as {username}

{gamePhase === "connected" && ( )}
{status === "reconnecting" && (
Connection lost during a live match. Reconnect attempt # {reconnectAttempts}...
)} {tournamentMode && (
🏆 Tournament mode active
)} {(gamePhase === "playing" || gamePhase === "game-over") && myColor && (
You are {myColorLabel}
{gamePhase === "playing" && (
{isMyTurn ? "⬆ Your turn - click a column" : "⏳ Waiting for opponent..."}
)}
)}
{gamePhase === "idle" ? (
Connect from the connection page to start.
) : gamePhase === "connected" ? (

Ready up to start

Click the{" "} Ready Up {" "} button beside your status to enter the queue.

) : gamePhase === "ready" ? (

Waiting for an opponent...

) : ( <> {gameResult && (
{gameResult === "win" ? "🏆 You Won!" : gameResult === "loss" ? "💔 You Lost" : gameResult === "draw" ? "🤝 Draw" : "⛔ Match Terminated"}
)} )}
); } function PhaseIndicator({ phase, isMyTurn, }: { phase: GamePhase; isMyTurn: boolean; }) { if (phase === "playing" && isMyTurn) { return ( Your Turn! ); } const config: Record = { idle: { label: "Not ready", cls: "bg-gray-700 text-gray-400" }, connected: { label: "Connected", cls: "bg-blue-900/60 text-blue-300" }, ready: { label: "Waiting...", cls: "bg-yellow-900/60 text-yellow-300 animate-pulse", }, playing: { label: "In Game", cls: "bg-green-900/60 text-green-400" }, "game-over": { label: "Game Over", cls: "bg-gray-700 text-gray-400" }, }; const { label, cls } = config[phase]; return ( {label} ); }