From 42fb17a1e6cf364a643b5eda29efe7582331e5e1 Mon Sep 17 00:00:00 2001 From: Henrichy Date: Wed, 29 Jul 2026 09:21:16 +0100 Subject: [PATCH 1/2] feat(frontend): lazy-load chess.js & StockfishWASM via next/dynamic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract heavy chess engine logic into dedicated wrapper components so the main JS bundle no longer eagerly ships chess.js or the Stockfish bridge on first paint. - Create HeroChessGame wrapper encapsulating Chess + useStockfishWASM + matchmaking/socket logic for the landing page - Create PlayGameEngine wrapper for /play/[slug] containing Chess, cheat detection, online clocks, and game UI - Use next/dynamic with ssr:false + skeleton loaders to mount each engine only when the user enters the corresponding game context - Trim app/page.tsx from 661 → 101 lines and app/play/[slug]/page.tsx from 499 → 60 lines; both are now thin router shells Fixes: initial-page chunk bloat from chess.js + StockfishWASM bridge --- frontend/app/page.tsx | 670 ++----------------- frontend/app/play/[slug]/page.tsx | 517 ++------------ frontend/components/chess/HeroChessGame.tsx | 588 ++++++++++++++++ frontend/components/chess/PlayGameEngine.tsx | 473 +++++++++++++ 4 files changed, 1155 insertions(+), 1093 deletions(-) create mode 100644 frontend/components/chess/HeroChessGame.tsx create mode 100644 frontend/components/chess/PlayGameEngine.tsx diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index f6d60a2..1885443 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,144 +1,60 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import dynamic from "next/dynamic"; -const ChessboardComponent = dynamic( - () => import("@/components/chess/ChessboardComponent"), - { ssr: false }, -); -import { Chess } from "chess.js"; -const GameModeButtons = dynamic(() => import("@/components/GameModeButtons"), { - ssr: false, -}); -const AiPersonalityModal = dynamic( - () => - import("@/app/components/matchmaking/AiPersonalityModal").then((m) => ({ - default: m.AiPersonalityModal, - })), - { ssr: false }, -); -const MatchmakingModal = dynamic( - () => - import("@/app/components/matchmaking/MatchmakingModal").then((m) => ({ - default: m.MatchmakingModal, - })), - { ssr: false }, -); -import { FaUser } from "react-icons/fa"; -import { RiAliensFill } from "react-icons/ri"; -import { useChessSocket } from "@/hook/useChessSocket"; -import { useMatchmaking } from "@/hook/useMatchmaking"; -import { useStockfishWASM, AnalysisResult } from "@/components/chess/StockfishWASM"; -import { useRouter } from "next/navigation"; -import { useMatchmakingContext } from "@/context/matchmakingContext"; -import { ChessVariantSelector } from "@/components/ChessVariantSelector"; -import { getChessVariantById } from "@/lib/chessVariants"; import { HeroBranding } from "@/components/HeroBranding"; -import { GameResultOverlay } from "@/components/GameResultOverlay"; -import type { GameResult } from "@/components/GameResultOverlay"; import { WalletConnectModal } from "@/components/WalletConnectModal"; -import { CapturedPieces } from "@/components/chess/CapturedPieces"; -import { EvaluationBar } from "@/components/chess/EvaluationBar"; -import { MoveHistory } from "@/components/chess/MoveHistory"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; import { endpoints } from "@/lib/api"; +const HeroChessGame = dynamic( + () => import("@/components/chess/HeroChessGame"), + { + ssr: false, + loading: () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+
+
+ ), + }, +); + export default function Home() { - const [game] = useState(new Chess()); - const [position, setPosition] = useState("start"); - const [viewIndex, setViewIndex] = useState(null); - const [gameMode, setGameMode] = useState<"online" | "bot" | null>(null); - const router = useRouter(); const [onlinePlayerCount, setOnlinePlayerCount] = useState(null); const PLAYER_COUNT_ENDPOINT = endpoints.players.online(); - const [isPersonalityModalOpen, setIsPersonalityModalOpen] = useState(false); - const [isMatchmakingModalOpen, setIsMatchmakingModalOpen] = useState(false); const [isWalletModalOpen, setIsWalletModalOpen] = useState(false); - const [botAnalysis, setBotAnalysis] = useState(null); - - // Hero game state (instant-play on landing) - const [heroGameResult, setHeroGameResult] = useState(null); - const [heroMoveCount, setHeroMoveCount] = useState(0); - - const { aiPersonality, chessVariant, setChessVariant } = - useMatchmakingContext(); - const selectedVariant = getChessVariantById(chessVariant); - - const { - status: matchmakingStatus, - playerColor, - error: matchmakingError, - joinMatchmaking, - cancelMatchmaking, - sendMove: matchmakingSendMove, - lastOpponentMove, - gameId, - } = useMatchmaking(); - - const { - status: socketStatus, - sendMove: socketSendMove, - disconnect: disconnectSocket, - reconnect: reconnectSocket, - } = useChessSocket(gameId); - - const { analyzePosition, isReady: stockfishReady, isAnalyzing } = useStockfishWASM({ - jsBridgePath: "/assets/stockfish.js", - defaultTimeLimit: 250, - }); - - const currentDisplayPosition = React.useMemo(() => { - if (viewIndex === null) return position; - const tempGame = new Chess(); - const history = game.history(); - for (let i = 0; i <= viewIndex; i++) { - try { tempGame.move(history[i]); } catch {} - } - return tempGame.fen(); - }, [position, viewIndex, game]); - - const handleMoveClick = useCallback((index: number) => { - setViewIndex(index === game.history().length - 1 ? null : index); - }, [game]); - - // Choose which sendMove to use based on game state - const sendMove = useCallback( - (from: string, to: string, promotion?: string) => { - if (gameId) { - socketSendMove({ from, to, promotion: promotion || "q" }); - } else { - matchmakingSendMove(from, to, promotion); - } - }, - [gameId, socketSendMove, matchmakingSendMove], - ); - - // Kick off matchmaking → route to game - useEffect(() => { - if (matchmakingStatus === "match_found" && gameId) { - router.push(`/play/${gameId}`); - } - }, [matchmakingStatus, gameId, router]); - // Apply opponent's move in online mode - useEffect(() => { - if (!lastOpponentMove || gameMode !== "online") return; - try { - const move = game.move({ - from: lastOpponentMove.from, - to: lastOpponentMove.to, - promotion: lastOpponentMove.promotion ?? "q", - }); - if (move) { - setPosition(game.fen()); - setViewIndex(null); - } - } catch { - // illegal move from server — ignore - } - }, [lastOpponentMove, game, gameMode]); - - // Fetch online player count useEffect(() => { let isMounted = true; const fetchOnlinePlayers = async () => { @@ -163,499 +79,23 @@ export default function Home() { }; }, [PLAYER_COUNT_ENDPOINT]); - // ─── HERO BOT: Stockfish auto-plays Black at easy depth ─── - useEffect(() => { - let active = true; - - const playBotMove = async () => { - // Only respond when: - // - No explicit game mode selected (hero mode) OR game mode is "bot" - // - It's Black's turn - // - Stockfish is ready - // - Game is not over - // - Not currently analyzing - const isHeroMode = gameMode === null; - const isBotMode = gameMode === "bot"; - if (!stockfishReady || isAnalyzing || !(isHeroMode || isBotMode)) return; - if (game.turn() !== "b" || game.isGameOver()) return; - - try { - // Easy depth for hero, deeper for explicit bot mode - let depth = 5; // easy for landing page - if (isBotMode) { - depth = 10; - if (aiPersonality === "aggressive") depth = 15; - if (aiPersonality === "defensive") depth = 18; - } - - const fenBeforeAnalysis = game.fen(); - const result = await analyzePosition(fenBeforeAnalysis, depth); - if ( - active && - result.bestMove && - game.fen() === fenBeforeAnalysis && - !game.isGameOver() - ) { - setBotAnalysis(result); - const from = result.bestMove.substring(0, 2); - const to = result.bestMove.substring(2, 4); - const promotion = - result.bestMove.length > 4 ? result.bestMove.substring(4, 5) : undefined; - game.move({ from, to, promotion }); - setPosition(game.fen()); - setViewIndex(null); - setHeroMoveCount((c) => c + 1); - } - } catch (e) { - console.error("Bot failed to move:", e); - } - }; - - // Small delay so the board animates the human move first - const timer = setTimeout(playBotMove, 400); - return () => { - active = false; - clearTimeout(timer); - }; - }, [position, gameMode, analyzePosition, aiPersonality, game, stockfishReady, isAnalyzing]); - - // ─── DETECT GAME OVER for hero / bot mode ─── - useEffect(() => { - if (!game.isGameOver()) return; - // Only show result overlay for hero mode or bot mode - if (gameMode !== null && gameMode !== "bot") return; - - if (game.isCheckmate()) { - // The side whose turn it is has been checkmated - setHeroGameResult(game.turn() === "w" ? "black_wins" : "white_wins"); - } else if (game.isStalemate()) { - setHeroGameResult("stalemate"); - } else { - setHeroGameResult("draw"); - } - }, [position, game, gameMode]); - - // ─── HERO MOVE HANDLER (always active) ─── - const isMyTurn = (() => { - if (gameMode === null) { - // Hero mode: White's turn only - return game.turn() === "w"; - } - if (gameMode === "bot") { - return game.turn() === "w"; - } - if (gameMode === "online") { - return ( - socketStatus === "connected" && - ((playerColor === "white" && game.turn() === "w") || - (playerColor === "black" && game.turn() === "b")) - ); - } - return true; - })(); - - const handleMove = useCallback( - ({ - sourceSquare, - targetSquare, - }: { - sourceSquare: string; - targetSquare: string; - }) => { - if (!isMyTurn || game.isGameOver()) return false; - if (viewIndex !== null) { - setViewIndex(null); - return false; - } - - try { - const move = game.move({ - from: sourceSquare, - to: targetSquare, - promotion: "q", - }); - if (move === null) return false; - - setBotAnalysis(null); - setHeroMoveCount((c) => c + 1); - requestAnimationFrame(() => setPosition(game.fen())); - // Forward move to server in online mode - if (gameMode === "online") { - sendMove(sourceSquare, targetSquare, "q"); - } - - setPosition(game.fen()); - setViewIndex(null); - return true; - } catch { - return false; - } - }, - [isMyTurn, game, gameMode, sendMove, viewIndex], + const heroBranding = ( + setIsWalletModalOpen(true)} + /> ); - // ─── HERO PLAY AGAIN ─── - const handleHeroPlayAgain = useCallback(() => { - game.reset(); - setPosition("start"); - setBotAnalysis(null); - setHeroGameResult(null); - setHeroMoveCount(0); - }, [game]); - - // ─── MODE SELECTION (below the fold) ─── - const handleExit = () => { - if (gameMode === "online") { - cancelMatchmaking(); - disconnectSocket(); - } - game.reset(); - setPosition("start"); - setGameMode(null); - setBotAnalysis(null); - setHeroGameResult(null); - setHeroMoveCount(0); - }; - - const handleSetGameMode = (mode: "online" | "bot" | null) => { - if (mode === "online") { - setGameMode(mode); - setIsMatchmakingModalOpen(true); - } else if (mode === "bot") { - setGameMode(mode); - setIsPersonalityModalOpen(true); - } else { - setGameMode(mode); - } - }; - - const handleMatchmakingConfirm = (type: "Rated" | "Casual") => { - setIsMatchmakingModalOpen(false); - joinMatchmaking(type); - }; - - const handleMatchmakingClose = () => { - setIsMatchmakingModalOpen(false); - setGameMode(null); - }; - - const handlePersonalityConfirm = () => { - setIsPersonalityModalOpen(false); - game.reset(); - setPosition("start"); - setBotAnalysis(null); - setHeroGameResult(null); - setHeroMoveCount(0); - }; - - const handlePersonalityClose = () => { - setIsPersonalityModalOpen(false); - setGameMode(null); - }; - - const handlePlayOnlineFromResult = () => { - setHeroGameResult(null); - handleSetGameMode("online"); - }; - - // Online status overlay label - const onlineStatusLabel = () => { - if (socketStatus === "reconnecting") return "🔄 Reconnecting..."; - if (matchmakingStatus === "match_found") return "✅ Match found! Starting…"; - if (socketStatus === "connected") - return `🟢 Online Match (you are ${playerColor})`; - if (matchmakingStatus === "error" || socketStatus === "error") - return `❌ ${matchmakingError ?? "Connection error"}`; - return "Online Match"; - }; - return ( -
- {/* ═══════════════════════════════════════════════════════════ */} - {/* HERO SECTION — Instant Play + Stellar Branding */} - {/* ═══════════════════════════════════════════════════════════ */} -
-
- {/* Chessboard — always interactive */} -
- {/* Turn indicator */} -
-
-
- - You (White) - -
- - {botAnalysis && ( -
- - Eval:{" "} - {botAnalysis.evaluation != null - ? `${botAnalysis.evaluation > 0 ? "+" : ""}${botAnalysis.evaluation.toFixed(2)}` - : "N/A"} - - | - D{botAnalysis.depth} -
- )} - -
- - Bot (Black) - -
-
-
- - {/* Captured Pieces by Bot (Black capturing White) */} -
- -
- - {/* The board and Eval bar */} -
- -
- - - -
-
- - {/* Captured Pieces by You (White capturing Black) */} -
- -
- - {/* Move counter */} -
- - {heroMoveCount > 0 - ? `Move ${Math.ceil(heroMoveCount / 2)}` - : "Make your first move!"} - - {!stockfishReady && ( - - - Loading engine... - - )} - {stockfishReady && heroMoveCount === 0 && ( - - - Engine ready - - )} -
-
- - {/* Move History */} -
- -
- - {/* Branding Panel */} -
- setIsWalletModalOpen(true)} - /> -
-
-
- - {/* ═══════════════════════════════════════════════════════════ */} - {/* GAME MODES SECTION — Below the fold */} - {/* ═══════════════════════════════════════════════════════════ */} -
- {/* Section header */} -
-

- Choose Your Arena -

-

- Play online, train with bots, or solve puzzles to earn XLM -

-
- -
- {/* Game Mode Buttons */} -
- - -
-
-
- - {/* ═══════════════════════════════════════════════════════════ */} - {/* ACTIVE GAME OVERLAYS */} - {/* ═══════════════════════════════════════════════════════════ */} - - {/* Game mode active bar */} - {gameMode && ( -
-
-
- {gameMode === "online" ? ( - - ) : ( - - )} -
-
-

- {gameMode === "online" - ? onlineStatusLabel() - : "Playing vs Bot"} -

-

- {selectedVariant.label} / {selectedVariant.averageGameTime} -

-
-
- -
- )} - - {/* Searching overlay */} - {gameMode === "online" && matchmakingStatus === "searching" && ( -
-
-
-

- Looking for opponent... -

- - - - -

- {onlinePlayerCount} Players online -

-

- Queueing for {selectedVariant.label} -

- -
-
-
- )} - - {/* Reconnecting overlay */} - {gameMode === "online" && socketStatus === "reconnecting" && ( -
-
-
-
-

- Reconnecting... -

-

- Attempting to restore connection -

- -
-
-
- )} - - {/* Game Result Overlay */} - {heroGameResult && ( - - )} - - {/* Modals */} - - + setIsWalletModalOpen(false)} /> -
+ ); } diff --git a/frontend/app/play/[slug]/page.tsx b/frontend/app/play/[slug]/page.tsx index d918cda..a73c14e 100644 --- a/frontend/app/play/[slug]/page.tsx +++ b/frontend/app/play/[slug]/page.tsx @@ -1,499 +1,60 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React from "react"; import dynamic from "next/dynamic"; -import { Chess } from "chess.js"; -import { useParams, useRouter } from "next/navigation"; -import { useChessSocket } from "@/hook/useChessSocket"; -import { FaUser, FaClock, FaSignal } from "react-icons/fa"; -import { Web3StatusBar } from "@/components/Web3StatusBar"; -import { useCheatDetection } from "@/hook/useCheatDetection"; -import { GameResultOverlay } from "@/components/GameResultOverlay"; -import type { GameResult } from "@/components/GameResultOverlay"; -import { CheatDetectionPanel } from "@/components/chess/CheatDetectionPanel"; -import { useIsMobile } from "@/hook/use-mobile"; -import { ErrorBoundary } from "@/components/ErrorBoundary"; -const ChessboardComponent = dynamic( - () => import("@/components/chess/ChessboardComponent"), +const PlayGameEngine = dynamic( + () => import("@/components/chess/PlayGameEngine"), { ssr: false, loading: () => ( -
-
- {Array.from({ length: 64 }).map((_, i) => ( -
- ))} -
-
- ), - }, -); - -type GameStatus = "playing" | "checkmate" | "stalemate" | "draw" | "resigned"; - -export default function PlayOnlinePage() { - const params = useParams(); - const router = useRouter(); - const gameId = params.slug as string; - - const [game] = useState(new Chess()); - const [position, setPosition] = useState("start"); - const [moveHistory, setMoveHistory] = useState([]); - const [whiteTime, setWhiteTime] = useState(600); // 10 min in seconds - const [blackTime, setBlackTime] = useState(600); - const [playerColor] = useState<"white" | "black">("white"); - const [gameStatus, setGameStatus] = useState("playing"); - const [isCheatPanelExpanded, setIsCheatPanelExpanded] = useState(false); - const [isMoveHistoryOpen, setIsMoveHistoryOpen] = useState(false); - const [boardOrientation, setBoardOrientation] = useState<"white" | "black">("white"); - const isMobile = useIsMobile(); - - const handleFlipBoard = useCallback(() => { - setBoardOrientation((prev) => (prev === "white" ? "black" : "white")); - }, []); - - const { - status: socketStatus, - sendMove, - disconnect, - reconnect, - lastOpponentMove, - } = useChessSocket(gameId); - - const checkGameStatus = useCallback(() => { - if (game.isCheckmate()) { - setGameStatus("checkmate"); - } else if (game.isStalemate()) { - setGameStatus("stalemate"); - } else if (game.isDraw()) { - setGameStatus("draw"); - } - }, [game]); - - // ── FE-04: Countdown clocks ─────────────────────────────────────────────── - // Tick the active side's clock down by 1 second every second while the game - // is live. The interval is cleared the moment the game ends. - useEffect(() => { - if (socketStatus !== "connected" || gameStatus !== "playing") return; - - const id = setInterval(() => { - const activeColor = game.turn(); // "w" | "b" - if (activeColor === "w") { - setWhiteTime((t) => Math.max(0, t - 1)); - } else { - setBlackTime((t) => Math.max(0, t - 1)); - } - }, 1000); - - return () => clearInterval(id); - }, [socketStatus, gameStatus, game]); - - // Sync clocks from authoritative server clock messages. - // The socket emits { type: "clock", whiteTime: number, blackTime: number } - // after each move so both sides stay in sync. - useEffect(() => { - // useChessSocket only exposes lastOpponentMove; clock data arrives via the - // raw WebSocket. We listen for a "clock" message relayed through a custom - // DOM event dispatched by a thin shim (or we handle it here directly). - // For now we seed from lastOpponentMove's accompanying clock field when - // the server includes it. - if (!lastOpponentMove) return; - const raw = lastOpponentMove as typeof lastOpponentMove & { - whiteTime?: number; - blackTime?: number; - }; - if (typeof raw.whiteTime === "number") setWhiteTime(raw.whiteTime); - if (typeof raw.blackTime === "number") setBlackTime(raw.blackTime); - }, [lastOpponentMove]); - - // Cheat detection - const { - opponentAnalysis, - playerAnalysis, - recordMove: recordCheatMove, - reset: resetCheatDetection, - isActive: isCheatDetectionActive, - } = useCheatDetection(playerColor); - - // Apply opponent's move to local chess state - useEffect(() => { - if (!lastOpponentMove) return; - try { - const fenBeforeOpponentMove = game.fen(); - const move = game.move({ - from: lastOpponentMove.from, - to: lastOpponentMove.to, - promotion: lastOpponentMove.promotion ?? "q", - }); - if (move) { - setPosition(game.fen()); - setMoveHistory((prev: string[]) => [...prev, move.san]); - recordCheatMove( - move.san, - move, - fenBeforeOpponentMove, - playerColor === "white" ? "b" : "w", - Math.ceil(game.moveNumber() / 2), - ); - checkGameStatus(); - } - } catch { - // illegal move from server — ignore - } - }, [lastOpponentMove, game, checkGameStatus, recordCheatMove, playerColor]); - - const isMyTurn = - socketStatus === "connected" && - ((playerColor === "white" && game.turn() === "w") || - (playerColor === "black" && game.turn() === "b")); - - const handleMove = useCallback( - ({ - sourceSquare, - targetSquare, - }: { - sourceSquare: string; - targetSquare: string; - }) => { - if (!isMyTurn || gameStatus !== "playing") return false; - - try { - const fenBeforeMyMove = game.fen(); - const move = game.move({ - from: sourceSquare, - to: targetSquare, - promotion: "q", - }); - if (move === null) return false; - - requestAnimationFrame(() => setPosition(game.fen())); - setMoveHistory((prev: string[]) => [...prev, move.san]); - sendMove({ from: sourceSquare, to: targetSquare, promotion: "q" }); - recordCheatMove( - move.san, - move, - fenBeforeMyMove, - playerColor === "white" ? "w" : "b", - Math.ceil(game.moveNumber() / 2), - ); - checkGameStatus(); - return true; - } catch { - return false; - } - }, - [isMyTurn, game, gameStatus, sendMove, checkGameStatus, recordCheatMove, playerColor], - ); - - const handleResign = useCallback(() => { - setGameStatus("resigned"); - resetCheatDetection(); - disconnect(); - }, [disconnect, resetCheatDetection]); - - const formatTime = (seconds: number) => { - const m = Math.floor(seconds / 60); - const s = seconds % 60; - return `${m}:${s.toString().padStart(2, "0")}`; - }; - - const socketStatusLabel = () => { - switch (socketStatus) { - case "connected": - return "Live"; - case "connecting": - return "Connecting..."; - case "reconnecting": - return "Reconnecting..."; - case "disconnected": - return "Disconnected"; - case "error": - return "Error"; - default: - return "Idle"; - } - }; - - const socketStatusColor = () => { - switch (socketStatus) { - case "connected": - return "text-emerald-400"; - case "connecting": - case "reconnecting": - return "text-yellow-400"; - default: - return "text-red-400"; - } - }; - - // Group moves into pairs for display - const movePairs = moveHistory.reduce( - (acc: string[][], move: string, i: number) => { - if (i % 2 === 0) acc.push([move]); - else acc[acc.length - 1].push(move); - return acc; - }, - [], - ); - - let overlayResult: GameResult | null = null; - if (gameStatus === "checkmate") { - overlayResult = game.turn() === "b" ? "white_wins" : "black_wins"; - } else if (gameStatus === "stalemate") { - overlayResult = "stalemate"; - } else if (gameStatus === "draw") { - overlayResult = "draw"; - } - - return ( -
-
- {/* Top bar */} -
- - -
- -
- {/* Chessboard Section */} -
- {/* Opponent info bar */} -
-
-
- -
-
-

Opponent

-

- {playerColor === "white" ? "Black" : "White"} -

-
-
-
- - - {formatTime(playerColor === "white" ? blackTime : whiteTime)} - -
-
- - {/* Board */} -
- - - -
- - {/* Player info bar */} -
-
-
- -
-
-

You

-

- {playerColor} - {isMyTurn && ( - - (Your turn) - - )} -

-
-
-
- - - {formatTime(playerColor === "white" ? whiteTime : blackTime)} - -
-
- - {/* Mobile controls — visible only on mobile, directly below board */} -
- - -
- - - {socketStatusLabel()} - -
-
+
+
+
+
+
- - {/* Game Sidebar - Move History & Controls */} -
- {/* Game Status Card */} -
-
-

- Game Status -

-
- - - {socketStatusLabel()} - +
+
+
+
+
+
+
+
+
-
- - {gameStatus !== "playing" && ( -
-

- {gameStatus === "checkmate" && "Checkmate!"} - {gameStatus === "stalemate" && "Stalemate!"} - {gameStatus === "draw" && "Draw!"} - {gameStatus === "resigned" && "Resigned!"} -

-
- )} - - {game.isCheck() && gameStatus === "playing" && ( -
-

Check!

+
+
- )} - -
- Game ID: {gameId?.slice(0, 12)}...
-
- - {/* Move History */} -
- -
- {movePairs.length === 0 ? ( -

- No moves yet.{" "} - {isMyTurn ? "Your turn to move!" : "Waiting for opponent..."} -

- ) : ( - movePairs.map((pair, i) => ( +
+
+ {Array.from({ length: 64 }).map((_, i) => (
- - {i + 1}. - - - {pair[0]} - - - {pair[1] ?? ""} - -
- )) - )} + className={`${ + (Math.floor(i / 8) + (i % 8)) % 2 === 0 + ? "bg-gray-700/30" + : "bg-gray-600/20" + } rounded-sm shimmer-bg`} + /> + ))} +
- - {/* Cheat Detection Panel */} - setIsCheatPanelExpanded((prev: boolean) => !prev)} - /> - - {/* Controls — desktop only, mobile controls are above the sidebar */} -
- - +
+
+
+
- - {/* Reconnection button */} - {socketStatus === "disconnected" && gameStatus === "playing" && ( - - )}
+ ), + }, +); - {overlayResult && ( - router.push("/")} - onPlayOnline={() => router.push("/")} - /> - )} -
- ); +export default function PlayOnlinePage() { + return ; } diff --git a/frontend/components/chess/HeroChessGame.tsx b/frontend/components/chess/HeroChessGame.tsx new file mode 100644 index 0000000..ee3ef5f --- /dev/null +++ b/frontend/components/chess/HeroChessGame.tsx @@ -0,0 +1,588 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import dynamic from "next/dynamic"; +import { Chess } from "chess.js"; +import { FaUser } from "react-icons/fa"; +import { RiAliensFill } from "react-icons/ri"; +import { useChessSocket } from "@/hook/useChessSocket"; +import { useMatchmaking } from "@/hook/useMatchmaking"; +import { useStockfishWASM, AnalysisResult } from "@/components/chess/StockfishWASM"; +import { useRouter } from "next/navigation"; +import { useMatchmakingContext } from "@/context/matchmakingContext"; +import { getChessVariantById } from "@/lib/chessVariants"; +import { GameResultOverlay } from "@/components/GameResultOverlay"; +import type { GameResult } from "@/components/GameResultOverlay"; +import { CapturedPieces } from "@/components/chess/CapturedPieces"; +import { EvaluationBar } from "@/components/chess/EvaluationBar"; +import { MoveHistory } from "@/components/chess/MoveHistory"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; + +const ChessboardComponent = dynamic( + () => import("@/components/chess/ChessboardComponent"), + { ssr: false }, +); +const GameModeButtons = dynamic(() => import("@/components/GameModeButtons"), { + ssr: false, +}); +const AiPersonalityModal = dynamic( + () => + import("@/app/components/matchmaking/AiPersonalityModal").then((m) => ({ + default: m.AiPersonalityModal, + })), + { ssr: false }, +); +const MatchmakingModal = dynamic( + () => + import("@/app/components/matchmaking/MatchmakingModal").then((m) => ({ + default: m.MatchmakingModal, + })), + { ssr: false }, +); +const ChessVariantSelector = dynamic( + () => import("@/components/ChessVariantSelector"), + { ssr: false }, +); + +interface HeroChessGameProps { + onlinePlayerCount: number | null; + heroBranding: React.ReactNode; +} + +export default function HeroChessGame({ + onlinePlayerCount, + heroBranding, +}: HeroChessGameProps) { + const [game] = useState(new Chess()); + const [position, setPosition] = useState("start"); + const [viewIndex, setViewIndex] = useState(null); + const [gameMode, setGameMode] = useState<"online" | "bot" | null>(null); + const router = useRouter(); + const [isPersonalityModalOpen, setIsPersonalityModalOpen] = useState(false); + const [isMatchmakingModalOpen, setIsMatchmakingModalOpen] = useState(false); + const [botAnalysis, setBotAnalysis] = useState(null); + + const [heroGameResult, setHeroGameResult] = useState(null); + const [heroMoveCount, setHeroMoveCount] = useState(0); + + const { aiPersonality, chessVariant, setChessVariant } = + useMatchmakingContext(); + const selectedVariant = getChessVariantById(chessVariant); + + const { + status: matchmakingStatus, + playerColor, + error: matchmakingError, + joinMatchmaking, + cancelMatchmaking, + sendMove: matchmakingSendMove, + lastOpponentMove, + gameId, + } = useMatchmaking(); + + const { + status: socketStatus, + sendMove: socketSendMove, + disconnect: disconnectSocket, + reconnect: reconnectSocket, + } = useChessSocket(gameId); + + const { analyzePosition, isReady: stockfishReady, isAnalyzing } = useStockfishWASM({ + jsBridgePath: "/assets/stockfish.js", + defaultTimeLimit: 250, + }); + + const currentDisplayPosition = React.useMemo(() => { + if (viewIndex === null) return position; + const tempGame = new Chess(); + const history = game.history(); + for (let i = 0; i <= viewIndex; i++) { + try { tempGame.move(history[i]); } catch {} + } + return tempGame.fen(); + }, [position, viewIndex, game]); + + const handleMoveClick = useCallback((index: number) => { + setViewIndex(index === game.history().length - 1 ? null : index); + }, [game]); + + const sendMove = useCallback( + (from: string, to: string, promotion?: string) => { + if (gameId) { + socketSendMove({ from, to, promotion: promotion || "q" }); + } else { + matchmakingSendMove(from, to, promotion); + } + }, + [gameId, socketSendMove, matchmakingSendMove], + ); + + useEffect(() => { + if (matchmakingStatus === "match_found" && gameId) { + router.push(`/play/${gameId}`); + } + }, [matchmakingStatus, gameId, router]); + + useEffect(() => { + if (!lastOpponentMove || gameMode !== "online") return; + try { + const move = game.move({ + from: lastOpponentMove.from, + to: lastOpponentMove.to, + promotion: lastOpponentMove.promotion ?? "q", + }); + if (move) { + setPosition(game.fen()); + setViewIndex(null); + } + } catch { + // illegal move from server — ignore + } + }, [lastOpponentMove, game, gameMode]); + + useEffect(() => { + let active = true; + + const playBotMove = async () => { + const isHeroMode = gameMode === null; + const isBotMode = gameMode === "bot"; + if (!stockfishReady || isAnalyzing || !(isHeroMode || isBotMode)) return; + if (game.turn() !== "b" || game.isGameOver()) return; + + try { + let depth = 5; + if (isBotMode) { + depth = 10; + if (aiPersonality === "aggressive") depth = 15; + if (aiPersonality === "defensive") depth = 18; + } + + const fenBeforeAnalysis = game.fen(); + const result = await analyzePosition(fenBeforeAnalysis, depth); + if ( + active && + result.bestMove && + game.fen() === fenBeforeAnalysis && + !game.isGameOver() + ) { + setBotAnalysis(result); + const from = result.bestMove.substring(0, 2); + const to = result.bestMove.substring(2, 4); + const promotion = + result.bestMove.length > 4 ? result.bestMove.substring(4, 5) : undefined; + game.move({ from, to, promotion }); + setPosition(game.fen()); + setViewIndex(null); + setHeroMoveCount((c) => c + 1); + } + } catch (e) { + console.error("Bot failed to move:", e); + } + }; + + const timer = setTimeout(playBotMove, 400); + return () => { + active = false; + clearTimeout(timer); + }; + }, [position, gameMode, analyzePosition, aiPersonality, game, stockfishReady, isAnalyzing]); + + useEffect(() => { + if (!game.isGameOver()) return; + if (gameMode !== null && gameMode !== "bot") return; + + if (game.isCheckmate()) { + setHeroGameResult(game.turn() === "w" ? "black_wins" : "white_wins"); + } else if (game.isStalemate()) { + setHeroGameResult("stalemate"); + } else { + setHeroGameResult("draw"); + } + }, [position, game, gameMode]); + + const isMyTurn = (() => { + if (gameMode === null) { + return game.turn() === "w"; + } + if (gameMode === "bot") { + return game.turn() === "w"; + } + if (gameMode === "online") { + return ( + socketStatus === "connected" && + ((playerColor === "white" && game.turn() === "w") || + (playerColor === "black" && game.turn() === "b")) + ); + } + return true; + })(); + + const handleMove = useCallback( + ({ + sourceSquare, + targetSquare, + }: { + sourceSquare: string; + targetSquare: string; + }) => { + if (!isMyTurn || game.isGameOver()) return false; + if (viewIndex !== null) { + setViewIndex(null); + return false; + } + + try { + const move = game.move({ + from: sourceSquare, + to: targetSquare, + promotion: "q", + }); + if (move === null) return false; + + setBotAnalysis(null); + setHeroMoveCount((c) => c + 1); + requestAnimationFrame(() => setPosition(game.fen())); + if (gameMode === "online") { + sendMove(sourceSquare, targetSquare, "q"); + } + + setPosition(game.fen()); + setViewIndex(null); + return true; + } catch { + return false; + } + }, + [isMyTurn, game, gameMode, sendMove, viewIndex], + ); + + const handleHeroPlayAgain = useCallback(() => { + game.reset(); + setPosition("start"); + setBotAnalysis(null); + setHeroGameResult(null); + setHeroMoveCount(0); + }, [game]); + + const handleExit = () => { + if (gameMode === "online") { + cancelMatchmaking(); + disconnectSocket(); + } + game.reset(); + setPosition("start"); + setGameMode(null); + setBotAnalysis(null); + setHeroGameResult(null); + setHeroMoveCount(0); + }; + + const handleSetGameMode = (mode: "online" | "bot" | null) => { + if (mode === "online") { + setGameMode(mode); + setIsMatchmakingModalOpen(true); + } else if (mode === "bot") { + setGameMode(mode); + setIsPersonalityModalOpen(true); + } else { + setGameMode(mode); + } + }; + + const handleMatchmakingConfirm = (type: "Rated" | "Casual") => { + setIsMatchmakingModalOpen(false); + joinMatchmaking(type); + }; + + const handleMatchmakingClose = () => { + setIsMatchmakingModalOpen(false); + setGameMode(null); + }; + + const handlePersonalityConfirm = () => { + setIsPersonalityModalOpen(false); + game.reset(); + setPosition("start"); + setBotAnalysis(null); + setHeroGameResult(null); + setHeroMoveCount(0); + }; + + const handlePersonalityClose = () => { + setIsPersonalityModalOpen(false); + setGameMode(null); + }; + + const handlePlayOnlineFromResult = () => { + setHeroGameResult(null); + handleSetGameMode("online"); + }; + + const onlineStatusLabel = () => { + if (socketStatus === "reconnecting") return "🔄 Reconnecting..."; + if (matchmakingStatus === "match_found") return "✅ Match found! Starting…"; + if (socketStatus === "connected") + return `🟢 Online Match (you are ${playerColor})`; + if (matchmakingStatus === "error" || socketStatus === "error") + return `❌ ${matchmakingError ?? "Connection error"}`; + return "Online Match"; + }; + + return ( +
+
+
+
+
+
+
+ + You (White) + +
+ + {botAnalysis && ( +
+ + Eval:{" "} + {botAnalysis.evaluation != null + ? `${botAnalysis.evaluation > 0 ? "+" : ""}${botAnalysis.evaluation.toFixed(2)}` + : "N/A"} + + | + D{botAnalysis.depth} +
+ )} + +
+ + Bot (Black) + +
+
+
+ +
+ +
+ +
+ +
+ + + +
+
+ +
+ +
+ +
+ + {heroMoveCount > 0 + ? `Move ${Math.ceil(heroMoveCount / 2)}` + : "Make your first move!"} + + {!stockfishReady && ( + + + Loading engine... + + )} + {stockfishReady && heroMoveCount === 0 && ( + + + Engine ready + + )} +
+
+ +
+ +
+ +
+ {heroBranding} +
+
+
+ +
+
+

+ Choose Your Arena +

+

+ Play online, train with bots, or solve puzzles to earn XLM +

+
+ +
+
+ + +
+
+
+ + {gameMode && ( +
+
+
+ {gameMode === "online" ? ( + + ) : ( + + )} +
+
+

+ {gameMode === "online" + ? onlineStatusLabel() + : "Playing vs Bot"} +

+

+ {selectedVariant.label} / {selectedVariant.averageGameTime} +

+
+
+ +
+ )} + + {gameMode === "online" && matchmakingStatus === "searching" && ( +
+
+
+

+ Looking for opponent... +

+ + + + +

+ {onlinePlayerCount} Players online +

+

+ Queueing for {selectedVariant.label} +

+ +
+
+
+ )} + + {gameMode === "online" && socketStatus === "reconnecting" && ( +
+
+
+
+

+ Reconnecting... +

+

+ Attempting to restore connection +

+ +
+
+
+ )} + + {heroGameResult && ( + + )} + + + +
+ ); +} diff --git a/frontend/components/chess/PlayGameEngine.tsx b/frontend/components/chess/PlayGameEngine.tsx new file mode 100644 index 0000000..ca9b83c --- /dev/null +++ b/frontend/components/chess/PlayGameEngine.tsx @@ -0,0 +1,473 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import dynamic from "next/dynamic"; +import { Chess } from "chess.js"; +import { useParams, useRouter } from "next/navigation"; +import { useChessSocket } from "@/hook/useChessSocket"; +import { FaUser, FaClock, FaSignal } from "react-icons/fa"; +import { Web3StatusBar } from "@/components/Web3StatusBar"; +import { useCheatDetection } from "@/hook/useCheatDetection"; +import { GameResultOverlay } from "@/components/GameResultOverlay"; +import type { GameResult } from "@/components/GameResultOverlay"; +import { CheatDetectionPanel } from "@/components/chess/CheatDetectionPanel"; +import { useIsMobile } from "@/hook/use-mobile"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; + +const ChessboardComponent = dynamic( + () => import("@/components/chess/ChessboardComponent"), + { + ssr: false, + loading: () => ( +
+
+ {Array.from({ length: 64 }).map((_, i) => ( +
+ ))} +
+
+ ), + }, +); + +type GameStatus = "playing" | "checkmate" | "stalemate" | "draw" | "resigned"; + +export default function PlayGameEngine() { + const params = useParams(); + const router = useRouter(); + const gameId = params.slug as string; + + const [game] = useState(new Chess()); + const [position, setPosition] = useState("start"); + const [moveHistory, setMoveHistory] = useState([]); + const [whiteTime, setWhiteTime] = useState(600); + const [blackTime, setBlackTime] = useState(600); + const [playerColor] = useState<"white" | "black">("white"); + const [gameStatus, setGameStatus] = useState("playing"); + const [isCheatPanelExpanded, setIsCheatPanelExpanded] = useState(false); + const [isMoveHistoryOpen, setIsMoveHistoryOpen] = useState(false); + const [boardOrientation, setBoardOrientation] = useState<"white" | "black">("white"); + const isMobile = useIsMobile(); + + const handleFlipBoard = useCallback(() => { + setBoardOrientation((prev) => (prev === "white" ? "black" : "white")); + }, []); + + const { + status: socketStatus, + sendMove, + disconnect, + reconnect, + lastOpponentMove, + } = useChessSocket(gameId); + + const checkGameStatus = useCallback(() => { + if (game.isCheckmate()) { + setGameStatus("checkmate"); + } else if (game.isStalemate()) { + setGameStatus("stalemate"); + } else if (game.isDraw()) { + setGameStatus("draw"); + } + }, [game]); + + useEffect(() => { + if (socketStatus !== "connected" || gameStatus !== "playing") return; + + const id = setInterval(() => { + const activeColor = game.turn(); + if (activeColor === "w") { + setWhiteTime((t) => Math.max(0, t - 1)); + } else { + setBlackTime((t) => Math.max(0, t - 1)); + } + }, 1000); + + return () => clearInterval(id); + }, [socketStatus, gameStatus, game]); + + useEffect(() => { + if (!lastOpponentMove) return; + const raw = lastOpponentMove as typeof lastOpponentMove & { + whiteTime?: number; + blackTime?: number; + }; + if (typeof raw.whiteTime === "number") setWhiteTime(raw.whiteTime); + if (typeof raw.blackTime === "number") setBlackTime(raw.blackTime); + }, [lastOpponentMove]); + + const { + opponentAnalysis, + playerAnalysis, + recordMove: recordCheatMove, + reset: resetCheatDetection, + isActive: isCheatDetectionActive, + } = useCheatDetection(playerColor); + + useEffect(() => { + if (!lastOpponentMove) return; + try { + const fenBeforeOpponentMove = game.fen(); + const move = game.move({ + from: lastOpponentMove.from, + to: lastOpponentMove.to, + promotion: lastOpponentMove.promotion ?? "q", + }); + if (move) { + setPosition(game.fen()); + setMoveHistory((prev: string[]) => [...prev, move.san]); + recordCheatMove( + move.san, + move, + fenBeforeOpponentMove, + playerColor === "white" ? "b" : "w", + Math.ceil(game.moveNumber() / 2), + ); + checkGameStatus(); + } + } catch { + // illegal move from server — ignore + } + }, [lastOpponentMove, game, checkGameStatus, recordCheatMove, playerColor]); + + const isMyTurn = + socketStatus === "connected" && + ((playerColor === "white" && game.turn() === "w") || + (playerColor === "black" && game.turn() === "b")); + + const handleMove = useCallback( + ({ + sourceSquare, + targetSquare, + }: { + sourceSquare: string; + targetSquare: string; + }) => { + if (!isMyTurn || gameStatus !== "playing") return false; + + try { + const fenBeforeMyMove = game.fen(); + const move = game.move({ + from: sourceSquare, + to: targetSquare, + promotion: "q", + }); + if (move === null) return false; + + requestAnimationFrame(() => setPosition(game.fen())); + setMoveHistory((prev: string[]) => [...prev, move.san]); + sendMove({ from: sourceSquare, to: targetSquare, promotion: "q" }); + recordCheatMove( + move.san, + move, + fenBeforeMyMove, + playerColor === "white" ? "w" : "b", + Math.ceil(game.moveNumber() / 2), + ); + checkGameStatus(); + return true; + } catch { + return false; + } + }, + [isMyTurn, game, gameStatus, sendMove, checkGameStatus, recordCheatMove, playerColor], + ); + + const handleResign = useCallback(() => { + setGameStatus("resigned"); + resetCheatDetection(); + disconnect(); + }, [disconnect, resetCheatDetection]); + + const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return `${m}:${s.toString().padStart(2, "0")}`; + }; + + const socketStatusLabel = () => { + switch (socketStatus) { + case "connected": + return "Live"; + case "connecting": + return "Connecting..."; + case "reconnecting": + return "Reconnecting..."; + case "disconnected": + return "Disconnected"; + case "error": + return "Error"; + default: + return "Idle"; + } + }; + + const socketStatusColor = () => { + switch (socketStatus) { + case "connected": + return "text-emerald-400"; + case "connecting": + case "reconnecting": + return "text-yellow-400"; + default: + return "text-red-400"; + } + }; + + const movePairs = moveHistory.reduce( + (acc: string[][], move: string, i: number) => { + if (i % 2 === 0) acc.push([move]); + else acc[acc.length - 1].push(move); + return acc; + }, + [], + ); + + let overlayResult: GameResult | null = null; + if (gameStatus === "checkmate") { + overlayResult = game.turn() === "b" ? "white_wins" : "black_wins"; + } else if (gameStatus === "stalemate") { + overlayResult = "stalemate"; + } else if (gameStatus === "draw") { + overlayResult = "draw"; + } + + return ( +
+
+
+ + +
+ +
+
+
+
+
+ +
+
+

Opponent

+

+ {playerColor === "white" ? "Black" : "White"} +

+
+
+
+ + + {formatTime(playerColor === "white" ? blackTime : whiteTime)} + +
+
+ +
+ + + +
+ +
+
+
+ +
+
+

You

+

+ {playerColor} + {isMyTurn && ( + + (Your turn) + + )} +

+
+
+
+ + + {formatTime(playerColor === "white" ? whiteTime : blackTime)} + +
+
+ +
+ + +
+ + + {socketStatusLabel()} + +
+
+
+ +
+
+
+

+ Game Status +

+
+ + + {socketStatusLabel()} + +
+
+ + {gameStatus !== "playing" && ( +
+

+ {gameStatus === "checkmate" && "Checkmate!"} + {gameStatus === "stalemate" && "Stalemate!"} + {gameStatus === "draw" && "Draw!"} + {gameStatus === "resigned" && "Resigned!"} +

+
+ )} + + {game.isCheck() && gameStatus === "playing" && ( +
+

Check!

+
+ )} + +
+ Game ID: {gameId?.slice(0, 12)}... +
+
+ +
+ +
+ {movePairs.length === 0 ? ( +

+ No moves yet.{" "} + {isMyTurn ? "Your turn to move!" : "Waiting for opponent..."} +

+ ) : ( + movePairs.map((pair, i) => ( +
+ + {i + 1}. + + + {pair[0]} + + + {pair[1] ?? ""} + +
+ )) + )} +
+
+ + setIsCheatPanelExpanded((prev: boolean) => !prev)} + /> + +
+ + +
+ + {socketStatus === "disconnected" && gameStatus === "playing" && ( + + )} +
+
+
+ + {overlayResult && ( + router.push("/")} + onPlayOnline={() => router.push("/")} + /> + )} +
+ ); +} From 11cfc982228c4fe1c6686e02b6b458176b5d1f30 Mon Sep 17 00:00:00 2001 From: Henrichy Date: Tue, 4 Aug 2026 00:42:00 +0100 Subject: [PATCH 2/2] new correction --- frontend/app/page.tsx | 54 +-- frontend/components/chess/HeroChessGame.tsx | 7 +- frontend/components/route.md' | 440 -------------------- frontend/package-lock.json | 88 ++-- 4 files changed, 41 insertions(+), 548 deletions(-) delete mode 100644 frontend/components/route.md' diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 5bf0fc6..8f0f311 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import dynamic from "next/dynamic"; const ChessboardComponent = dynamic( () => import("@/components/chess/ChessboardComponent"), @@ -39,50 +39,11 @@ import { getChessVariantById } from "@/lib/chessVariants"; import { HeroBranding } from "@/components/HeroBranding"; import { WalletConnectModal } from "@/components/WalletConnectModal"; import { endpoints } from "@/lib/api"; - -const HeroChessGame = dynamic( - () => import("@/components/chess/HeroChessGame"), - { - ssr: false, - loading: () => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 64 }).map((_, i) => ( -
- ))} -
-
-
-
-
-
-
-
-
- ), - }, -); +import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { CapturedPieces } from "@/components/chess/CapturedPieces"; +import { EvaluationBar } from "@/components/chess/EvaluationBar"; +import { MoveHistory } from "@/components/chess/MoveHistory"; +import GameResultOverlay, { type GameResult } from "@/components/GameResultOverlay"; export default function Home() { const [game] = useState(new Chess()); @@ -116,7 +77,6 @@ export default function Home() { joinMatchmaking, cancelMatchmaking, sendMove: matchmakingSendMove, - lastOpponentMove, gameId, } = useMatchmaking(); @@ -701,6 +661,6 @@ export default function Home() { isOpen={isWalletModalOpen} onClose={() => setIsWalletModalOpen(false)} /> - +
); } diff --git a/frontend/components/chess/HeroChessGame.tsx b/frontend/components/chess/HeroChessGame.tsx index ee3ef5f..0679969 100644 --- a/frontend/components/chess/HeroChessGame.tsx +++ b/frontend/components/chess/HeroChessGame.tsx @@ -40,7 +40,10 @@ const MatchmakingModal = dynamic( { ssr: false }, ); const ChessVariantSelector = dynamic( - () => import("@/components/ChessVariantSelector"), + () => + import("@/components/ChessVariantSelector").then((m) => ({ + default: m.ChessVariantSelector, + })), { ssr: false }, ); @@ -291,7 +294,7 @@ export default function HeroChessGame({ const handleMatchmakingConfirm = (type: "Rated" | "Casual") => { setIsMatchmakingModalOpen(false); - joinMatchmaking(type); + joinMatchmaking(type, chessVariant); }; const handleMatchmakingClose = () => { diff --git a/frontend/components/route.md' b/frontend/components/route.md' deleted file mode 100644 index 0fe453d..0000000 --- a/frontend/components/route.md' +++ /dev/null @@ -1,440 +0,0 @@ -# Route - -## Features Implemented - -This document summarizes the Route of the requested AI/Infra features for the KnightVerse chess platform. - ---- - -## ✅ 1. Natural Language Agent Interface - -### Overview -A conversational interface that allows users to interact with chess engines using plain English, providing intelligent analysis, suggestions, and explanations. - -### Files Created - -#### Core Modules -- **`agent-engines/gpu_worker/nl_models.py`** (119 lines) - - Data models for NL requests and responses - - Intent type enumeration (7 intent types) - - Complexity level support (beginner, intermediate, advanced) - - Move analysis and intent recognition models - -- **`agent-engines/gpu_worker/nl_intent_parser.py`** (184 lines) - - Pattern-based intent recognition system - - Regex-based entity extraction - - FEN string detection and parsing - - Chess move extraction from natural language - - Confidence scoring algorithm - -- **`agent-engines/gpu_worker/nl_agent.py`** (571 lines) - - Main NaturalLanguageAgent service class - - Intent-based request routing (7 handlers) - - Multi-level complexity responses - - Integration with chess engine worker pool - - Request history tracking - - Natural language response generation - -#### Tests -- **`agent-engines/tests/test_nl_agent.py`** (428 lines) - - Unit tests for all models - - Intent recognition tests (20+ test cases) - - Complexity detection tests - - Entity extraction tests - - Full agent workflow tests - - Edge case coverage - -### Features -- ✅ Intent Recognition: 7 distinct intent types - - Analyze position - - Suggest move - - Explain move - - Get hint - - Compare moves - - Learn concept - - Unknown intent handling - -- ✅ Complexity Levels - - Beginner: Simple, jargon-free explanations - - Intermediate: Balanced technical detail - - Advanced: Full engine output with variations - -- ✅ Entity Extraction - - FEN string detection - - Algebraic notation move extraction - - Context preservation - -- ✅ Response Generation - - Position evaluations in natural language - - Move suggestions with reasoning - - Tactical and strategic hints - - Concept explanations (forks, pins, skewers) - -### Usage Example -```python -from gpu_worker.nl_agent import NaturalLanguageAgent - -agent = NaturalLanguageAgent(worker_pool) - -# Move suggestion -response = await agent.process_request( - user_input="What's the best move?", - fen="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" -) -print(response.natural_language_response) -print(f"Best move: {response.best_move}") -``` - ---- - -## ✅ 2. CI/CD Pipeline for Soroban Deployments - -### Overview -Automated deployment pipeline for Stellar Soroban smart contracts with rollback support, verification, and multi-environment deployment. - -### Files Created - -#### GitHub Actions Workflow -- **`.github/workflows/soroban-deploy.yml`** (286 lines) - - Multi-stage pipeline (validate → deploy → verify → notify) - - Support for testnet and futurenet - - Manual and automatic triggers - - WASM validation and inspection - - Artifact preservation (30-day retention) - - Deployment summary generation - -#### Deployment Scripts -- **`contracts/deploy_advanced.sh`** (359 lines) - - Production-ready deployment with rollback - - Automatic backup creation (max 10 backups) - - Deployment verification with retry logic - - Contract ID persistence - - Deployment history logging - - Multiple commands: deploy, rollback, verify, history, backups, build - -### Features -- ✅ Automated Testing & Validation - - Contract unit tests - - WASM file validation - - Soroban contract inspection - -- ✅ Multi-Environment Support - - Testnet (automatic on main branch) - - Futurenet (manual trigger) - - Network configuration management - -- ✅ Rollback Mechanism - - Automatic backups before deployment - - One-command rollback to previous version - - Backup cleanup (keeps last 10) - - Deployment history tracking - -- ✅ Verification System - - Post-deployment health checks - - Contract accessibility verification - - Retry logic (5 attempts) - - Automatic rollback on verification failure - -- ✅ CI/CD Integration - - GitHub Actions workflow - - Secret management for deployer keys - - Environment protection rules - - Deployment notifications - -### Usage Examples - -#### Automated Deployment (GitHub Actions) -```yaml -# Triggers automatically on push to main -push: - branches: [main] - paths: ['contracts/**'] - -# Or manual trigger -workflow_dispatch: - inputs: - environment: - type: choice - options: [testnet, futurenet] -``` - -#### Manual Deployment -```bash -# Deploy all contracts -./deploy_advanced.sh deploy testnet - -# Deploy specific contract -./deploy_advanced.sh deploy testnet game_contract testnet-deployer - -# Rollback -./deploy_advanced.sh rollback testnet game_contract - -# Verify -./deploy_advanced.sh verify testnet game_contract - -# History -./deploy_advanced.sh history -``` - -### Required GitHub Secrets -- `SOROBAN_SECRET_KEY`: Testnet deployer key -- `SOROBAN_FUTURENET_SECRET_KEY`: Futurenet deployer key - ---- - -## ✅ 3. Stockfish 16.1 Integration via WASM - -### Overview -WebAssembly-based Stockfish integration enabling browser-compatible chess engine analysis without server dependencies. - -### Files Created - -#### Core Modules -- **`agent-engines/gpu_worker/stockfish_wasm.py`** (408 lines) - - StockfishWASMEngine class - - WASM engine configuration - - Async analysis interface - - Concurrent position analysis - - JavaScript bridge code generator - - WASM download information - -- **`agent-engines/gpu_worker/stockfish_wasm_bridge.py`** (389 lines) - - TypeScript bridge code generator - - React hook (useStockfishWASM) - - Analysis display component - - Engine status indicator - -#### Frontend Component -- **`frontend/components/chess/StockfishWASM.tsx`** (358 lines) - - React TypeScript component - - useStockfishWASM hook - - AnalysisDisplay component - - EngineStatus component - - Web Worker integration - - Error handling and timeouts - -#### Tests -- **`agent-engines/tests/test_stockfish_wasm.py`** (381 lines) - - Configuration model tests - - Analysis result tests - - Engine lifecycle tests - - Concurrent analysis tests - - Error handling tests - - Resource cleanup tests - -### Features -- ✅ WASM Engine Integration - - Stockfish 16.1 support - - WebAssembly loading and initialization - - Configurable threads and hash size - - Skill level adjustment (0-20) - -- ✅ Analysis Capabilities - - Single position analysis - - Multiple concurrent analyses - - Configurable depth and time limits - - Principal variation extraction - - Evaluation scoring - -- ✅ Browser Compatibility - - Web Worker integration - - Shared Array Buffer support - - Memory management - - Graceful error handling - -- ✅ React Integration - - Custom hook (useStockfishWASM) - - Status indicators - - Analysis display components - - Loading states - - Error boundaries - -- ✅ Resource Management - - Proper cleanup on shutdown - - Timeout handling - - Worker termination - - Memory limit enforcement - -### Usage Examples - -#### Python Backend -```python -from gpu_worker.stockfish_wasm import StockfishWASMEngine, WASMEngineConfig - -engine = StockfishWASMEngine( - WASMEngineConfig(threads=2, hash_size_mb=32) -) - -await engine.initialize() - -result = await engine.analyze_position( - fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - depth=20 -) - -print(f"Best move: {result.best_move}") -print(f"Evaluation: {result.evaluation}") - -await engine.shutdown() -``` - -#### React Frontend -```typescript -import { useStockfishWASM, AnalysisDisplay } from '@/components/chess/StockfishWASM'; - -function ChessPanel() { - const { isReady, analyzePosition } = useStockfishWASM({ - defaultDepth: 18, - threads: 2, - }); - - const handleAnalyze = async () => { - const result = await analyzePosition(fen); - console.log('Best:', result.bestMove); - }; - - return ; -} -``` - -### WASM File Setup -```bash -# Download Stockfish WASM -# Source: https://github.com/nmrugg/stockfish.js - -# Place in public directory -cp stockfish.js frontend/public/assets/ -cp stockfish.wasm frontend/public/assets/ -``` - ---- - -## Testing - -### Run All Tests -```bash -cd agent-engines - -# Run all tests -pytest - -# Natural Language Agent tests -pytest tests/test_nl_agent.py -v - -# Stockfish WASM tests -pytest tests/test_stockfish_wasm.py -v - -# With coverage -pytest --cov=gpu_worker --cov-report=html -``` - -### Test Coverage -- ✅ Natural Language Agent: 25+ test cases -- ✅ Intent Parser: 15+ test cases -- ✅ Stockfish WASM: 20+ test cases -- ✅ Edge cases and error handling -- ✅ Concurrent operations -- ✅ Resource cleanup - ---- - -## Documentation - -### Updated Files -- **`agent-engines/README.md`** - - Added Natural Language Agent section - - Added Stockfish WASM section - - Added Soroban CI/CD section - - Usage examples for all features - - Testing instructions - -### Documentation Quality -- ✅ Comprehensive API documentation -- ✅ Code comments and docstrings -- ✅ Usage examples -- ✅ Installation instructions -- ✅ Configuration guides - ---- - -## Acceptance Criteria Checklist - -### Code Quality -- ✅ Well-documented code with docstrings -- ✅ Follows existing design patterns -- ✅ Type hints throughout -- ✅ Clean code structure -- ✅ Error handling - -### Testing -- ✅ Unit tests for all new modules -- ✅ Edge case coverage -- ✅ Integration tests -- ✅ Async test support -- ✅ Mocked external dependencies - -### Integration -- ✅ Fully integrated with existing codebase -- ✅ Compatible with cargo test (Rust backend) -- ✅ Compatible with pytest (Python agents) -- ✅ Frontend components ready for npm -- ✅ CI/CD workflows functional - -### Resource Efficiency -- ✅ Configurable resource limits -- ✅ Proper cleanup and shutdown -- ✅ Concurrent operation support -- ✅ Memory management -- ✅ Timeout handling - ---- - -## File Summary - -### New Files Created: 10 -1. `agent-engines/gpu_worker/nl_models.py` - NL agent data models -2. `agent-engines/gpu_worker/nl_intent_parser.py` - Intent recognition -3. `agent-engines/gpu_worker/nl_agent.py` - NL agent service -4. `agent-engines/gpu_worker/stockfish_wasm.py` - WASM engine integration -5. `agent-engines/gpu_worker/stockfish_wasm_bridge.py` - TypeScript generator -6. `agent-engines/tests/test_nl_agent.py` - NL agent tests -7. `agent-engines/tests/test_stockfish_wasm.py` - WASM tests -8. `.github/workflows/soroban-deploy.yml` - CI/CD pipeline -9. `contracts/deploy_advanced.sh` - Deployment script -10. `frontend/components/chess/StockfishWASM.tsx` - React component - -### Modified Files: 1 -1. `agent-engines/README.md` - Updated documentation - -### Total Lines Added: ~3,500+ -- Python: ~2,400 lines -- TypeScript: ~700 lines -- YAML/Shell: ~650 lines -- Documentation: ~300 lines - ---- - -## Next Steps - -### Deployment -1. Set up GitHub secrets for Soroban deployment -2. Configure GitHub environments (testnet, futurenet) -3. Download Stockfish WASM files for frontend -4. Run test suite to verify all integrations - -### Optional Enhancements -- Add LLM integration for enhanced NL responses -- Implement real-time analysis streaming -- Add puzzle generation using WASM engine -- Create deployment dashboard -- Add performance benchmarks - ---- - -## Support - -For questions or issues: -- Check the updated README.md in agent-engines/ -- Review test files for usage examples -- Consult inline code documentation -- Refer to CI/CD workflow files for deployment details diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 80b1a6d..9d20b5b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -191,6 +191,7 @@ "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2035,6 +2036,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -2058,35 +2060,11 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } }, - "node_modules/@emnapi/core": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", - "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "2.0.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", - "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", @@ -2094,7 +2072,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2986,6 +2963,7 @@ "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.62.0" }, @@ -3438,9 +3416,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3458,9 +3433,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3478,9 +3450,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3498,9 +3467,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3518,9 +3484,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3538,9 +3501,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3862,6 +3822,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4471,6 +4432,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4489,6 +4451,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4499,6 +4462,7 @@ "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4572,6 +4536,7 @@ "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.27.0", "@typescript-eslint/types": "8.27.0", @@ -5269,6 +5234,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5803,6 +5769,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -6998,6 +6965,7 @@ "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7172,6 +7140,7 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -8656,6 +8625,7 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" @@ -9845,6 +9815,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10017,6 +9988,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10119,6 +10091,7 @@ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.6.0.tgz", "integrity": "sha512-2KHNpeg2SyaxXYq+xO1TM+tOtN9hViI41otJuiYiu6DRYGw+WMvDFDMP4aw7zIKRRm1xd0gizXuKWhb8iJYHBw==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "disposables": "^1.0.1", "dnd-core": "^2.6.0", @@ -10209,6 +10182,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -10229,7 +10203,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-remove-scroll": { "version": "2.6.3", @@ -10387,6 +10362,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -10409,7 +10385,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/recharts/node_modules/redux-thunk": { "version": "3.1.0", @@ -10830,6 +10807,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11543,7 +11521,8 @@ "version": "4.0.15", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -11689,6 +11668,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11928,6 +11908,7 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12151,6 +12132,7 @@ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", @@ -12345,9 +12327,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12369,9 +12348,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12393,9 +12369,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12417,9 +12390,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [