"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Board from "@/components/Board"; import { BoardState, ParsedMessage, cmd, createEmptyBoard, parseMessage, placeToken, } from "@/lib/protocol"; type ConnStatus = "idle" | "connecting" | "connected" | "disconnected"; type GamePhase = | "idle" // not connected or connected but no game | "connected" // connected, awaiting ready | "ready" // sent READY, waiting for match | "playing" // in a match | "game-over"; // match finished type GameResult = "win" | "loss" | "draw" | "terminated"; const DEFAULT_URL = "wss://connect4.abunchofknowitalls.com"; export default function PlayPage() { const [wsUrl, setWsUrl] = useState(DEFAULT_URL); const [username, setUsername] = useState(""); const [connStatus, setConnStatus] = useState("idle"); const [gamePhase, setGamePhase] = useState("idle"); const [myColor, setMyColor] = useState<1 | 2 | null>(null); // 1=red, 2=yellow 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 [moveCount, setMoveCount] = useState(0); const [statusMessages, setStatusMessages] = useState([]); const [tournamentMode, setTournamentMode] = useState(false); const [waitingForNextRound, setWaitingForNextRound] = useState(false); const wsRef = useRef(null); const myColorRef = useRef<1 | 2 | null>(null); const isMyTurnRef = useRef(false); const addStatus = (msg: string) => setStatusMessages((prev) => [ `[${new Date().toLocaleTimeString()}] ${msg}`, ...prev.slice(0, 29), ]); const send = useCallback((msg: string) => { if (wsRef.current?.readyState === WebSocket.OPEN) wsRef.current.send(msg); }, []); const resetGame = useCallback(() => { setBoard(createEmptyBoard()); setLastMove(null); setMoveCount(0); setMyColor(null); myColorRef.current = null; setIsMyTurn(false); isMyTurnRef.current = false; setGameResult(null); }, []); const handleColumnClick = useCallback( (col: number) => { if (!isMyTurnRef.current || gamePhase !== "playing") return; // Optimistically place the piece; server validates and replies const color = myColorRef.current!; setBoard((prev) => { const { board: next, row } = placeToken(prev, color, col); if (row === -1) return prev; // column full, ignore setLastMove({ column: col, row }); return next; }); setIsMyTurn(false); isMyTurnRef.current = false; setMoveCount((n) => n + 1); send(cmd.play(col)); addStatus(`You played column ${col}`); }, [gamePhase, send] ); const handleMessage = useCallback( (raw: string) => { const msg: ParsedMessage = parseMessage(raw); switch (msg.type) { case "CONNECT_ACK": setConnStatus("connected"); setGamePhase("connected"); addStatus(`✅ Connected as "${username}"`); break; case "ERROR": addStatus(`⚠ ${msg.message}`); break; case "READY_ACK": setGamePhase("ready"); addStatus("⏳ Waiting for an opponent…"); break; case "GAME_START": { resetGame(); const color: 1 | 2 = msg.goesFirst ? 1 : 2; setMyColor(color); myColorRef.current = color; setGamePhase("playing"); setWaitingForNextRound(false); const firstTurn = msg.goesFirst; setIsMyTurn(firstTurn); isMyTurnRef.current = firstTurn; addStatus( msg.goesFirst ? "🔴 You are Red — you go FIRST!" : "🟡 You are Yellow — wait for opponent's first move" ); 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; }); setMoveCount((n) => n + 1); setIsMyTurn(true); isMyTurnRef.current = true; addStatus(`Opponent played column ${msg.column} — your turn!`); break; } case "GAME_WINS": setGameResult("win"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; addStatus("🏆 You won!"); break; case "GAME_LOSS": setGameResult("loss"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; addStatus("💔 You lost"); break; case "GAME_DRAW": setGameResult("draw"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; addStatus("🤝 Draw!"); break; case "GAME_TERMINATED": setGameResult("terminated"); setGamePhase("game-over"); setIsMyTurn(false); isMyTurnRef.current = false; addStatus("⛔ Match terminated"); break; case "TOURNAMENT_START": setTournamentMode(true); addStatus(`🏆 Tournament started: ${msg.tournamentType}`); break; case "TOURNAMENT_END": addStatus("Round over — sending Ready for next round…"); setWaitingForNextRound(false); setGamePhase("connected"); resetGame(); // Auto-ready for next round (mirror the gameloop.py behavior) setTimeout(() => send(cmd.ready()), 500); setGamePhase("ready"); addStatus("⏳ Ready for next round…"); break; case "TOURNAMENT_CANCEL": setTournamentMode(false); setWaitingForNextRound(false); setGamePhase("connected"); resetGame(); addStatus("Tournament cancelled"); break; default: break; } }, [username, resetGame, send] ); const connect = useCallback(() => { if (!username.trim()) { addStatus("⚠ Please enter a username first"); return; } if (wsRef.current) wsRef.current.close(); setConnStatus("connecting"); setGamePhase("idle"); resetGame(); setStatusMessages([]); setTournamentMode(false); setWaitingForNextRound(false); const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => ws.send(cmd.connect(username.trim())); ws.onmessage = (e) => handleMessage(e.data as string); ws.onclose = () => { setConnStatus("disconnected"); setGamePhase("idle"); addStatus("Disconnected from server"); }; ws.onerror = () => addStatus("WebSocket error"); }, [username, wsUrl, handleMessage, resetGame]); const disconnect = useCallback(() => { send(cmd.disconnect()); setTimeout(() => wsRef.current?.close(), 200); }, [send]); const sendReady = useCallback(() => { send(cmd.ready()); setGamePhase("ready"); addStatus("⏳ Waiting for an opponent…"); }, [send]); useEffect(() => () => wsRef.current?.close(), []); // Derived display values const myColorLabel = myColor === 1 ? "🔴 Red" : myColor === 2 ? "🟡 Yellow" : null; const opponentColor: 1 | 2 | null = myColor === 1 ? 2 : myColor === 2 ? 1 : null; return (
{/* Header */}

🎮 Play Connect4

Connect as a player and compete in matches

{/* Left: controls + status */}
{/* Connection card */}

Connection

setWsUrl(e.target.value)} placeholder="wss://..." disabled={connStatus === "connected" || connStatus === "connecting"} />
setUsername(e.target.value)} placeholder="Enter your username" disabled={connStatus === "connected" || connStatus === "connecting"} onKeyDown={(e) => e.key === "Enter" && connStatus === "idle" && connect()} />
{connStatus !== "connected" ? ( ) : ( )}
{/* Game controls */} {connStatus === "connected" && (

Match

{tournamentMode && (
🏆 Tournament mode active
)} {gamePhase === "connected" && !tournamentMode && ( )} {gamePhase === "ready" && (
⏳ Waiting for opponent…
)} {(gamePhase === "playing" || gamePhase === "game-over") && myColor && (
You are {myColorLabel} {myColor === 1 && ( (1st) )}
{gamePhase === "playing" && (
{isMyTurn ? "⬆ Your turn — click a column!" : "⏳ Waiting for opponent…"}
)}
{moveCount} move{moveCount !== 1 ? "s" : ""} played
)} {gamePhase === "game-over" && gameResult && (
{gameResult === "win" ? "🏆 You Won!" : gameResult === "loss" ? "💔 You Lost" : gameResult === "draw" ? "🤝 Draw" : "⛔ Terminated"}
{!tournamentMode && ( )}
)}
)} {/* Status log */}

Status Log

{statusMessages.length === 0 ? (

Connect to start

) : ( statusMessages.map((m, i) => (

{m}

)) )}
{/* How to play */}

How to Play

  1. 1. Enter the server URL and your username, then click Connect
  2. 2. Click Ready to Play to queue for a match
  3. 3. When the game starts, click a column number to drop your piece
  4. 4. Connect 4 in a row (horizontal, vertical, or diagonal) to win!
{/* Right: board */}
{connStatus !== "connected" || gamePhase === "idle" ? (
🎮

Ready to play?

Enter your username and connect to the server to start a match

) : gamePhase === "ready" ? (

Waiting for an opponent…

The game will start automatically when a match is found

) : ( <> {gameResult && (
{gameResult === "win" ? "🏆 You Won!" : gameResult === "loss" ? "💔 You Lost" : gameResult === "draw" ? "🤝 Draw!" : "⛔ Match Terminated"}
)} {gamePhase === "playing" && (

{isMyTurn ? ( ⬆ Click a column to drop your piece ) : ( Waiting for opponent… )}

)} )}
); } function PhaseIndicator({ phase, isMyTurn, }: { phase: GamePhase; isMyTurn: boolean; }) { if (phase === "playing" && isMyTurn) { return ( Your Turn! ); } const config: Record = { idle: { label: "Not connected", 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} ); }