import { useEffect, useRef, useState } from "react"; import { useAgent } from "agents/react"; import { createRoot } from "react-dom/client"; import { Chess, type Square } from "chess.js"; import { Chessboard, type ChessboardOptions, type PieceDropHandlerArgs } from "react-chessboard"; import type { State as ServerState } from "./chess"; function usePlayerId() { const [pid] = useState(() => { const existing = localStorage.getItem("playerId"); if (existing) return existing; const id = crypto.randomUUID(); localStorage.setItem("playerId", id); return id; }); return pid; } type JoinReply = { ok: true; role: "w" | "b" | "spectator"; state: ServerState; }; function describeGameStatus(state: ServerState | null): string { if (!state) return "Connecting to game..."; switch (state.status) { case "waiting": return "Waiting for players"; case "active": return "In progress"; case "mate": return state.winner === "w" ? "Checkmate · White wins" : "Checkmate · Black wins"; case "draw": return "Draw"; case "resigned": return state.winner ? `${state.winner === "w" ? "White" : "Black"} wins by resignation` : "Game ended by resignation"; default: return state.status; } } type PlayerSlotProps = { label: string; playerId?: string; isCurrent: boolean; }; function PlayerSlot({ label, playerId, isCurrent }: PlayerSlotProps) { const connected = Boolean(playerId); const highlight = isCurrent ? "rgba(37, 99, 235, 0.12)" : connected ? "rgba(59, 130, 246, 0.1)" : "rgba(15, 23, 42, 0.04)"; const border = isCurrent ? "1px solid rgba(37, 99, 235, 0.6)" : "1px solid rgba(15, 23, 42, 0.12)"; return (
{label}
{connected ? ( <> {isCurrent ? "You" : "Player"} · {playerId!.slice(-8)} ) : ( "Waiting for player" )}
); } /** -------------------------- * Main App * -------------------------- */ function App() { const widgetGameId = typeof window.openai?.widgetState?.gameId === "string" && `${window.openai?.widgetState?.gameId === "string"}`.trim() ? `${window.openai?.widgetState?.gameId}`.trim() : ""; const playerId = usePlayerId(); const [gameId, setGameId] = useState( widgetGameId ? widgetGameId : null ); const [gameIdInput, setGameIdInput] = useState(widgetGameId); const [menuError, setMenuError] = useState(null); const gameRef = useRef(new Chess()); const [fen, setFen] = useState(gameRef.current.fen()); const [myColor, setMyColor] = useState<"w" | "b" | "spectator">("spectator"); const [pending, setPending] = useState(false); const [serverState, setServerState] = useState(null); const [joined, setJoined] = useState(false); useEffect(() => { if (!gameId && widgetGameId) { setGameId(widgetGameId); } if (!gameIdInput && widgetGameId) { setGameIdInput(widgetGameId); } }, [widgetGameId, gameId, gameIdInput]); function resetLocalGame() { gameRef.current.reset(); setFen(gameRef.current.fen()); setPending(false); setMyColor("spectator"); setServerState(null); } const activeGameName = gameId ?? "__lobby__"; const host = window.HOST ?? "http://localhost:5174/"; const { stub } = useAgent({ host, name: activeGameName, agent: "chess", onStateUpdate: (s) => { if (!gameId) return; gameRef.current.load(s.board); setFen(s.board); setServerState(s); } }); useEffect(() => { if (!gameId || joined) return; (async () => { try { const res = (await stub.join({ playerId, preferred: "any" })) as JoinReply; if (!res?.ok) return; setMyColor(res.role); gameRef.current.load(res.state.board); setFen(res.state.board); setServerState(res.state); setJoined(true); } catch (error) { console.error("Failed to join game", error); } })(); }, [playerId, gameId, stub, joined]); async function handleStartNewGame() { const newId = crypto.randomUUID(); await window.openai?.setWidgetState({ gameId: newId }); resetLocalGame(); setMenuError(null); setGameIdInput(newId); setGameId(newId); } async function handleJoinGame() { const trimmed = gameIdInput.trim(); if (!trimmed) { setMenuError("Enter a game ID to join."); return; } resetLocalGame(); setMenuError(null); await window.openai?.setWidgetState({ gameId: trimmed }); setGameId(trimmed); } // Trigger a message on the ChatGPT conversation to help with the current board state const handleHelpClick = () => { window.openai?.sendFollowUpMessage?.({ prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice as there are no tools for you to use.` }); }; const handleResign = async () => { await stub.resign(); }; // Local-then-server move with reconcile function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs): boolean { if (!gameId || !sourceSquare || !targetSquare || pending) return false; const game = gameRef.current; // must be seated and your turn if (myColor === "spectator") return false; if (game.turn() !== myColor) return false; // must be your piece const piece = game.get(sourceSquare as Square); if (!piece || piece.color !== myColor) return false; const prevFen = game.fen(); try { const local = game.move({ from: sourceSquare, to: targetSquare, promotion: "q" }); if (!local) return false; } catch { return false; } const nextFen = game.fen(); setFen(nextFen); setPending(true); // reconcile with server stub .move({ from: sourceSquare, to: targetSquare, promotion: "q" }, prevFen) .then((r: { ok: boolean; fen: string }) => { if (!r.ok) { // rollback to server position game.load(r.fen); setFen(r.fen); } }) .finally(() => setPending(false)); return true; } const chessboardOptions: ChessboardOptions = { id: `pvp-${activeGameName}`, position: fen, onPieceDrop, boardOrientation: myColor === "b" ? "black" : "white", allowDragging: !pending && myColor !== "spectator" }; const maxSize = window.openai?.maxHeight ?? 650; const boardSize = Math.max(Math.min(maxSize - 120, 560), 320); const statusText = describeGameStatus(serverState); const whiteId = serverState?.players?.w; const blackId = serverState?.players?.b; return (
{!gameId ? (

Ready to play?

Start a new match to generate a shareable game code or join an existing game by pasting its ID below.

{ setGameIdInput(event.target.value); if (menuError) setMenuError(null); }} />
{menuError ? ( {menuError} ) : null}
) : (
Game {gameId}
{statusText}
{myColor === "spectator" ? "You are watching as a spectator" : playerId === serverState?.players[gameRef.current.turn()] ? "Your turn" : `Waiting for ${myColor === "w" ? "Black" : "White"}`}
Players
{myColor === "spectator" ? (
You're observing for now. We'll seat you automatically if a spot opens up.
) : null}
)}
); } const root = createRoot(document.getElementById("root")!); root.render();