").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;
}
}