import { Chess } from "../../../libs/chess.js"; import { Classification } from "../../classification/MoveClassifier.js"; import { MoveEvaluator } from "../../evaluation/MoveEvaluator.js"; import { EvaluationBar } from "../board/EvaluationBar.js"; export const IgnoredSuggestionTypes = [ Classification.BRILLIANT.type, Classification.GREAT.type, Classification.PERFECT.type, Classification.THEORY.type, Classification.FORCED.type ]; export class EngineLines { static updateEngineLines(node, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const $engineLines = $(".engine-lines").empty(); // Create title element const titleElement = this.createEngineTitleElement(node); $engineLines.append(titleElement); // Handle missing node if (!node) { this.showEngineWaitingMessage("Select a position to view analysis"); return; } // Handle root node with pre-computed evaluation if (node.id === 'root') { node.evaluatedMove = MoveEvaluator.startPositionEvaluation; node.fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; } // Handle game-over position const chess = new Chess(node.fen); if (chess.isGameOver()) { this.showGameOverMessage(chess); return; } // Handle missing evaluation data if (!node.evaluatedMove || !node.evaluatedMove.lines) { this.showEngineWaitingMessage("Analyzing position...", true); return; } // Display evaluation lines this.displayEngineLines(node, moveTree, handleTreeNodeClick, queueMoveForEvaluation); } static displayEngineLines(node, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const lines = node.evaluatedMove.lines; const $linesContainer = $("
").addClass("engine-lines-container"); const sortedLines = [...lines].sort((a, b) => a.id - b.id); // 3 lines only for (let i = 0; i < Math.min(3, sortedLines.length); i++) { const line = sortedLines[i]; if (!line) continue; const $lineContainer = this.createLineContainer(node, line, i, moveTree, handleTreeNodeClick, queueMoveForEvaluation); $linesContainer.append($lineContainer); } $(".engine-lines").append($linesContainer); } static showGameOverMessage(chess) { const $resultContainer = $("
").addClass("engine-lines-container"); const $resultLine = $("
").addClass("engine-line"); const $resultBox = $("
").addClass("game-result"); let resultText = ""; const isMate = chess.isCheckmate() if (isMate) { const turn = chess.turn() === 'w'; resultText = (turn ? "Black" : "White") + " won by checkmate"; if (turn) $resultBox.addClass("black"); } else if (chess.isStalemate()) { resultText = "Draw by stalemate"; } else if (chess.isInsufficientMaterial()) { resultText = "Draw by insufficient material"; } else if (chess.isThreefoldRepetition()) { resultText = "Draw by threefold repetition"; } else if (chess.isDraw()) { resultText = "Draw by 50-move rule"; } $resultBox.text(resultText); $resultLine.append($resultBox); $resultContainer.append($resultLine); $(".engine-lines").append($resultContainer); // Update evaluation bar based on result const evalObj = { evalScore: -0, evalType: isMate ? "mate" : "cp", mateForBlack: chess.turn() === 'w' && isMate }; EvaluationBar.updateEvaluationBar(evalObj); } static createLineContainer(node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const $lineContainer = $("
").addClass("engine-line"); const $scoreBox = this.createScoreBox(line); $lineContainer.append($scoreBox); const $movesContainer = $("
").addClass("engine-moves"); if (line.pv?.length > 0) { const { movesList, uciMovesList } = this.parsePrincipalVariation(node, line); this.createMovesContent($movesContainer, movesList, uciMovesList, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation); $lineContainer.attr({ "data-moves": JSON.stringify(movesList.map(m => m.moveObj)), "data-uci-moves": JSON.stringify(uciMovesList) }); } $lineContainer.append($movesContainer); return $lineContainer; } static createScoreBox(line) { let scoreText; if (line.type === "mate") { scoreText = (line.score > 0) ? "M" + line.score : "M" + Math.abs(line.score); } else { let evalValue = line.score / 100; scoreText = evalValue > 0 ? "+" + evalValue.toFixed(2) : evalValue.toFixed(2); } return $("
") .addClass("engine-score") .addClass(line.score >= 0 ? "white-score" : "black-score") .text(scoreText); } static parsePrincipalVariation(node, line) { const tempChess = new Chess(node.fen); const movesList = []; const uciMovesList = []; let moveNumber = Math.ceil(node.moveNumber); let isWhiteTurn = tempChess.turn() === 'w'; // Parse moves from UCI to SAN format line.pv.forEach(uciMove => { try { uciMovesList.push(uciMove); const from = uciMove.substring(0, 2); const to = uciMove.substring(2, 4); const promotion = uciMove.length > 4 ? uciMove.substring(4, 5) : undefined; const move = tempChess.move({ from, to, promotion }); //console.log(move); if (move) { // Create move prefix (number + dots) let prefix = ''; if (isWhiteTurn) { prefix = moveNumber + '. '; } else if (movesList.length === 0) { prefix = moveNumber + '... '; } movesList.push({ text: prefix + move.san, uci: uciMove, moveObj: { from, to, promotion }, index: movesList.length }); // Update move number and turn if (!isWhiteTurn) moveNumber++; isWhiteTurn = !isWhiteTurn; } } catch (e) { console.warn("Failed to parse UCI move:", uciMove, e); } }); return { movesList, uciMovesList }; } static createMovesContent($movesContainer, movesList, uciMovesList, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const maxVisibleMoves = 3; const $movesContentContainer = $("
").addClass("moves-content"); if (movesList.length > maxVisibleMoves) { // Create container for visible moves const visibleMovesList = movesList.slice(0, maxVisibleMoves); const hiddenMovesList = movesList.slice(maxVisibleMoves); // Add visible moves this.createVisibleMovesElements(visibleMovesList, $movesContentContainer, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation); $movesContainer.append($movesContentContainer); // Create expand button const $expandButton = $("") .addClass("expand-button") .html(''); $movesContainer.append($expandButton); // Create container for hidden moves const $hiddenMovesEl = this.createHiddenMovesElements(hiddenMovesList, node, line, lineIndex, maxVisibleMoves, moveTree, handleTreeNodeClick, queueMoveForEvaluation); $movesContentContainer.append($hiddenMovesEl); // Add expand/collapse functionality this.setupExpandCollapseHandler($expandButton, $hiddenMovesEl); } else { // If no truncation needed, just show all moves this.createSimpleMovesElements(movesList, $movesContentContainer, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation); $movesContainer.append($movesContentContainer); } } static createVisibleMovesElements(visibleMovesList, $container, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const $visibleMovesEl = $("").addClass("visible-moves"); visibleMovesList.forEach((moveData, idx) => { const $moveEl = $("") .addClass("clickable-move") .text(moveData.text) .attr({ "data-uci": moveData.uci, "data-position": idx, "data-line": lineIndex }) .on("click", () => { this.handleEngineLineClick(node, line, idx + 1, moveTree, handleTreeNodeClick, queueMoveForEvaluation); }); $visibleMovesEl.append($moveEl); if (idx < visibleMovesList.length - 1) { $visibleMovesEl.append(" "); } }); $container.append($visibleMovesEl); } static createHiddenMovesElements(hiddenMovesList, node, line, lineIndex, maxVisibleMoves, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const $hiddenMovesEl = $("
").addClass("hidden-moves"); hiddenMovesList.forEach((moveData, idx) => { const $moveEl = $("") .addClass("clickable-move") .text(moveData.text) .attr({ "data-uci": moveData.uci, "data-position": idx + maxVisibleMoves, "data-line": lineIndex }) .on("click", () => { this.handleEngineLineClick(node, line, idx + maxVisibleMoves + 1, moveTree, handleTreeNodeClick, queueMoveForEvaluation); }); $hiddenMovesEl.append($moveEl); if (idx < hiddenMovesList.length - 1) { $hiddenMovesEl.append(" "); } }); return $hiddenMovesEl; } static createSimpleMovesElements(movesList, $container, node, line, lineIndex, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { movesList.forEach((moveData, idx) => { const $moveEl = $("") .addClass("clickable-move") .text(moveData.text) .attr({ "data-uci": moveData.uci, "data-position": idx, "data-line": lineIndex }) .on("click", () => { this.handleEngineLineClick(node, line, idx + 1, moveTree, handleTreeNodeClick, queueMoveForEvaluation); }); $container.append($moveEl); if (idx < movesList.length - 1) { $container.append(" "); } }); } static setupExpandCollapseHandler($expandButton, $hiddenMovesEl) { $expandButton.on("click", function () { const isExpanded = $(this).hasClass("expanded"); $(this).toggleClass("expanded", !isExpanded); $(this).html(isExpanded ? `` : ``); $hiddenMovesEl[isExpanded ? 'slideUp' : 'slideDown'](200); }); } static createEngineTitleElement(node) { const hasEvaluation = (node?.evaluatedMove?.lines?.length > 0) || (node?.id === 'root'); const $title = $("
") .addClass("section-title engine-lines-title") .addClass(hasEvaluation ? "has-evaluation" : "no-evaluation") .append($("").text("Computer")); // Add depth info if available if (node?.evaluatedMove?.lines?.[0]) { const depth = node.evaluatedMove.lines[0].depth || "?"; const engine = node.evaluatedMove.engine || ""; $title.append( $("").addClass("engine-depth") .text("Depth " + depth + " " + engine) ); } return $title; } static showEngineWaitingMessage(message, isLoading = false) { const $waitingMsg = $("
") .addClass("engine-lines-waiting") .append( $("
") .append(``) .toggleClass("loading-icon", isLoading) ) .append($("").text(" " + message)); $(".engine-lines").append($waitingMsg); } static handleEngineLineClick(currentNode, line, movesToPlay, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { if (!line.pv?.length || movesToPlay <= 0 || movesToPlay > line.pv.length) { return; } const moves = line.pv.slice(0, movesToPlay); let chess = new Chess(currentNode.fen); let nodeToPlayFrom = currentNode; // Add each move as a variation for (let i = 0; i < moves.length; i++) { const uciMove = moves[i]; const from = uciMove.substring(0, 2); const to = uciMove.substring(2, 4); const promotion = uciMove.length > 4 ? uciMove.substring(4, 5) : undefined; // Try to make the move const moveObj = chess.move({ from, to, promotion }); console.log("engine line click", moveObj); if (!moveObj) return; // Check for existing node in mainline if nodeToPlayFrom is in mainline let existingNode = null; const nodeToPlayFromIndex = moveTree.getNodeIndex(nodeToPlayFrom); if (nodeToPlayFromIndex !== -1 && nodeToPlayFromIndex + 1 < moveTree.mainline.length) { const nextMainlineMove = moveTree.mainline[nodeToPlayFromIndex + 1]; if (nextMainlineMove.move && nextMainlineMove.move.from === moveObj.from && nextMainlineMove.move.to === moveObj.to && nextMainlineMove.move.promotion === moveObj.promotion) { existingNode = nextMainlineMove; } } // If not found in mainline, check for existing child in variations if (!existingNode) { existingNode = nodeToPlayFrom.children.find(child => child.move && child.move.from === moveObj.from && child.move.to === moveObj.to && child.move.promotion === moveObj.promotion ); } if (existingNode) { nodeToPlayFrom = existingNode; } else { // Create and evaluate new node nodeToPlayFrom = this.createEngineLineNode(moveObj, chess, nodeToPlayFrom, moveTree, handleTreeNodeClick, queueMoveForEvaluation); } } // Navigate to final node handleTreeNodeClick(nodeToPlayFrom); moveTree.render('move-tree', (node) => handleTreeNodeClick(node)); moveTree.updateCurrentMove(nodeToPlayFrom.id); } static createEngineLineNode(moveObj, chess, parentNode, moveTree, handleTreeNodeClick, queueMoveForEvaluation) { const newNode = moveTree.addMove(moveObj, parentNode.id); newNode.evaluationStatus = 'pending'; // Update UI moveTree.render('move-tree', (node) => handleTreeNodeClick(node)); moveTree.updateCurrentMove(newNode.id); // Queue for evaluation const fen = chess.fen(); const prevFen = parentNode.fen; queueMoveForEvaluation(newNode, fen, prevFen); return newNode; } }