// ==UserScript==
// @name Chess AI
// @namespace github.com/longkidkoolstar
// @version 2.1.0
// @description Chess.com Bot/Cheat that finds the best move with evaluation bar and ELO control!
// @author longkidkoolstar
// @license none
// @match https://www.chess.com/play/*
// @match https://www.chess.com/game/*
// @icon https://i.imgur.com/Z30WgSo.png
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.getResourceText
// @grant GM.download
// @resource stockfish.js https://raw.githubusercontent.com/longkidkoolstar/stockfish/refs/heads/main/stockfish.js
// @require https://greasyfork.org/scripts/445697/code/index.js
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @connect localhost
// @run-at document-start
// ==/UserScript==
const currentVersion = '2.1.0'; // Updated version number
function main() {
var stockfishObjectURL;
var engine = document.engine = {};
var myVars = document.myVars = {};
myVars.autoMovePiece = false;
myVars.autoRun = false;
myVars.delay = 0.1;
myVars.eloRating = 1500; // Default ELO rating
myVars.currentEvaluation = 0; // Current evaluation value
myVars.useVirtualChessboard = false; // Default to not using virtual chessboard
myVars.persistentHighlights = true; // Default to persistent highlights
myVars.moveIndicatorType = 'highlights'; // Default to highlights instead of arrows
myVars.showMultipleMoves = false; // Default to showing only the best move
myVars.numberOfMovesToShow = 3; // Default number of top moves to show
myVars.useMulticolorMoves = false; // Default to using opacity for move strength
myVars.useExternalWindow = false; // Default to not using external window
myVars.externalWindowOpen = false; // Track if external window is open
myVars.externalWindowRef = null; // Reference to external window
myVars.serverConnected = false; // Track if connected to local server
myVars.moveIndicatorLocation = 'main'; // Where to show move indicators: 'main', 'external', or 'both'
myVars.disableMainControls = false; // Option to disable main controls when connected to external window
myVars.autoQueue = false; // Default to not auto-queuing new games
// Default colors for multicolor mode
myVars.moveColors = {
1: '#F44336', // Red for best move
2: '#FF9800', // Orange for 2nd best
3: '#FFEB3B', // Yellow for 3rd best
4: '#4CAF50', // Green for 4th best
5: '#2196F3' // Blue for 5th best
}
var myFunctions = document.myFunctions = {};
// Function to download the Python server using GM.download with fallback
myFunctions.downloadServer = function() {
const serverUrl = 'https://raw.githubusercontent.com/longkidkoolstar/Chess-AI/refs/heads/main/chess_ai_server.py';
const filename = 'chess_ai_server.py';
// Show download notification
const notification = document.createElement('div');
notification.textContent = 'Downloading server file...';
notification.style = `
position: fixed;
bottom: 20px;
right: 20px;
background-color: #2196F3;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
}, 10);
// Try to use GM.download if available
try {
if (typeof GM.download === 'function') {
console.log('Using GM.download to download server file');
GM.download({
url: serverUrl,
name: filename,
onload: function() {
// Update notification to show success
notification.textContent = 'Server file downloaded successfully!';
notification.style.backgroundColor = '#4CAF50';
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 2000);
},
onerror: function() {
console.error('GM.download failed, falling back to direct download');
// Fall back to direct download
fallbackDownload();
}
});
} else {
console.log('GM.download not available, using fallback method');
fallbackDownload();
}
} catch (error) {
console.error('Error using GM.download:', error);
fallbackDownload();
}
// Fallback download method using window.open
function fallbackDownload() {
// Open the URL in a new tab
window.open(serverUrl, '_blank');
// Update notification to show instructions
notification.innerHTML = 'Please save the file as <strong>chess_ai_server.py</strong> when prompted';
notification.style.backgroundColor = '#FF9800';
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 5000);
}
};
// Create evaluation bar
var evalBar = null;
var evalText = null;
stop_b = stop_w = 0;
s_br = s_br2 = s_wr = s_wr2 = 0;
obs = "";
myFunctions.rescan = function(lev) {
var ari = $("chess-board")
.find(".piece")
.map(function() {
return this.className;
})
.get();
jack = ari.map(f => f.substring(f.indexOf(' ') + 1));
function removeWord(arr, word) {
for (var i = 0; i < arr.length; i++) {
arr[i] = arr[i].replace(word, '');
}
}
removeWord(ari, 'square-');
jack = ari.map(f => f.substring(f.indexOf(' ') + 1));
for (var i = 0; i < jack.length; i++) {
jack[i] = jack[i].replace('br', 'r')
.replace('bn', 'n')
.replace('bb', 'b')
.replace('bq', 'q')
.replace('bk', 'k')
.replace('bb', 'b')
.replace('bn', 'n')
.replace('br', 'r')
.replace('bp', 'p')
.replace('wp', 'P')
.replace('wr', 'R')
.replace('wn', 'N')
.replace('wb', 'B')
.replace('br', 'R')
.replace('wn', 'N')
.replace('wb', 'B')
.replace('wq', 'Q')
.replace('wk', 'K')
.replace('wb', 'B')
}
str2 = "";
var count = 0,
str = "";
for (var j = 8; j > 0; j--) {
for (var i = 1; i < 9; i++) {
(str = (jack.find(el => el.includes([i] + [j])))) ? str = str.replace(/[^a-zA-Z]+/g, ''): str = "";
if (str == "") {
count++;
str = count.toString();
if (!isNaN(str2.charAt(str2.length - 1))) str2 = str2.slice(0, -1);
else {
count = 1;
str = count.toString()
}
}
str2 += str;
if (i == 8) {
count = 0;
str2 += "/";
}
}
}
str2 = str2.slice(0, -1);
//str2=str2+" KQkq - 0"
color = "";
wk = wq = bk = bq = "0";
const move = $('vertical-move-list')
.children();
if (move.length < 2) {
stop_b = stop_w = s_br = s_br2 = s_wr = s_wr2 = 0;
}
if (stop_b != 1) {
if (move.find(".black.node:contains('K')")
.length) {
bk = "";
bq = "";
stop_b = 1;
console.log('debug secb');
}
} else {
bq = "";
bk = "";
}
if (stop_b != 1)(bk = (move.find(".black.node:contains('O-O'):not(:contains('O-O-O'))")
.length) ? "" : "k") ? (bq = (move.find(".black.node:contains('O-O-O')")
.length) ? bk = "" : "q") : bq = "";
if (s_br != 1) {
if (move.find(".black.node:contains('R')")
.text()
.match('[abcd]+')) {
bq = "";
s_br = 1
}
} else bq = "";
if (s_br2 != 1) {
if (move.find(".black.node:contains('R')")
.text()
.match('[hgf]+')) {
bk = "";
s_br2 = 1
}
} else bk = "";
if (stop_b == 0) {
if (s_br == 0)
if (move.find(".white.node:contains('xa8')")
.length > 0) {
bq = "";
s_br = 1;
console.log('debug b castle_r');
}
if (s_br2 == 0)
if (move.find(".white.node:contains('xh8')")
.length > 0) {
bk = "";
s_br2 = 1;
console.log('debug b castle_l');
}
}
if (stop_w != 1) {
if (move.find(".white.node:contains('K')")
.length) {
wk = "";
wq = "";
stop_w = 1;
console.log('debug secw');
}
} else {
wq = "";
wk = "";
}
if (stop_w != 1)(wk = (move.find(".white.node:contains('O-O'):not(:contains('O-O-O'))")
.length) ? "" : "K") ? (wq = (move.find(".white.node:contains('O-O-O')")
.length) ? wk = "" : "Q") : wq = "";
if (s_wr != 1) {
if (move.find(".white.node:contains('R')")
.text()
.match('[abcd]+')) {
wq = "";
s_wr = 1
}
} else wq = "";
if (s_wr2 != 1) {
if (move.find(".white.node:contains('R')")
.text()
.match('[hgf]+')) {
wk = "";
s_wr2 = 1
}
} else wk = "";
if (stop_w == 0) {
if (s_wr == 0)
if (move.find(".black.node:contains('xa1')")
.length > 0) {
wq = "";
s_wr = 1;
console.log('debug w castle_l');
}
if (s_wr2 == 0)
if (move.find(".black.node:contains('xh1')")
.length > 0) {
wk = "";
s_wr2 = 1;
console.log('debug w castle_r');
}
}
if ($('.coordinates')
.children()
.first()
.text() == 1) {
str2 = str2 + " b " + wk + wq + bk + bq;
color = "white";
} else {
str2 = str2 + " w " + wk + wq + bk + bq;
color = "black";
}
//console.log(str2);
return str2;
}
myFunctions.color = function(dat){
response = dat;
var res1 = response.substring(0, 2);
var res2 = response.substring(2, 4);
// Store the best move for server updates
myVars.bestMove = res1 + res2;
console.log('Best move set to:', myVars.bestMove);
// Add the move to history
const moveNotation = res1 + '-' + res2;
myFunctions.addMoveToHistory(moveNotation, myVars.currentEvaluation, lastValue);
// Clear any existing highlights and arrows before adding new ones
myFunctions.clearHighlights();
myFunctions.clearArrows();
// Also clear virtual chessboard indicators
myFunctions.clearVirtualMoveIndicators();
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
if(myVars.autoMove == true){
myFunctions.movePiece(res1, res2);
// After auto move, we need to reset canGo to allow auto run on next turn
setTimeout(() => {
canGo = true;
}, 500);
}
isThinking = false;
// Store the best move for reference
myVars.lastMove = {
from: res1,
to: res2,
algebraic: moveNotation
};
// Only show move indicators if the option is enabled
if(myVars.showArrows !== false) {
// Debug information
console.log("Multiple moves enabled:", myVars.showMultipleMoves);
console.log("Top moves array:", myVars.topMoves);
console.log("Virtual chessboard enabled:", myVars.useVirtualChessboard);
// If virtual chessboard is enabled, update it with the current position
if (myVars.useVirtualChessboard) {
// Make sure the virtual chessboard container is visible
const virtualChessboardContainer = document.getElementById('virtualChessboardContainer');
if (virtualChessboardContainer) {
virtualChessboardContainer.style.display = 'block';
}
// Update the virtual chessboard with the current position
myFunctions.updateVirtualChessboard();
// Show move indicators on the virtual chessboard
if (myVars.showMultipleMoves && myVars.topMoves && myVars.topMoves.length > 1) {
console.log("Showing multiple moves on virtual chessboard:", myVars.topMoves.length);
// Show multiple moves with varying opacity on virtual chessboard
const movesToShow = Math.min(myVars.numberOfMovesToShow, myVars.topMoves.length);
const bestEval = myVars.topMoves[0].evaluation;
for (let i = 0; i < movesToShow; i++) {
const moveInfo = myVars.topMoves[i];
const move = moveInfo.move;
// Skip if move is undefined
if (!move) continue;
const moveRes1 = move.substring(0, 2);
const moveRes2 = move.substring(2, 4);
// Variables for styling
let opacity = 0.9;
let moveColor = null;
if (myVars.useMulticolorMoves) {
// Use different colors for each move
moveColor = myVars.moveColors[i + 1] || getDefaultMoveColor(i);
opacity = 0.9;
} else {
// Calculate opacity based on relative strength
if (i > 0) {
if (!moveInfo.isMate && !myVars.topMoves[0].isMate) {
const relativeStrength = Math.max(0, 1 - Math.abs(bestEval - moveInfo.evaluation) / 3);
opacity = 0.3 + (relativeStrength * 0.6);
} else {
opacity = 0.9 - (i * 0.15);
}
}
opacity = Math.max(0.3, Math.min(0.9, opacity));
}
// Helper function to get default color for a move index
function getDefaultMoveColor(index) {
const defaultColors = [
'#F44336', // Red for best move
'#FF9800', // Orange for 2nd best
'#FFEB3B', // Yellow for 3rd best
'#4CAF50', // Green for 4th best
'#2196F3' // Blue for 5th best
];
return defaultColors[index] || '#9C27B0'; // Default to purple if out of range
}
// Get the color to use
let highlightColor = 'rgb(235, 97, 80)'; // Default red color
if (myVars.useMulticolorMoves) {
// Convert hex color to RGB for highlights
const moveColor = myVars.moveColors[i + 1] || getDefaultMoveColor(i);
highlightColor = hexToRgb(moveColor);
}
// Helper function to convert hex color to RGB format
function hexToRgb(hex) {
// Remove # if present
hex = hex.replace('#', '');
// Parse the hex values
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Return RGB format
return `rgb(${r}, ${g}, ${b})`;
}
// Show the move on the virtual chessboard
myFunctions.showVirtualMoveIndicator(moveRes1, moveRes2, opacity, highlightColor);
}
} else {
console.log("Showing single move on virtual chessboard");
// Show just the best move on virtual chessboard
myFunctions.showVirtualMoveIndicator(res1, res2);
}
} else {
// Hide the virtual chessboard container
const virtualChessboardContainer = document.getElementById('virtualChessboardContainer');
if (virtualChessboardContainer) {
virtualChessboardContainer.style.display = 'none';
}
// Show move indicators on the main board as before
if (myVars.showMultipleMoves && myVars.topMoves && myVars.topMoves.length > 1) {
console.log("Showing multiple moves:", myVars.topMoves.length);
// Show multiple moves with varying opacity
myFunctions.showMultipleMoveIndicators();
} else {
console.log("Showing single move - reason:",
!myVars.showMultipleMoves ? "Multiple moves disabled" :
!myVars.topMoves ? "No top moves array" :
myVars.topMoves.length <= 1 ? "Not enough moves in array" : "Unknown");
// Show just the best move (original behavior)
myFunctions.showSingleMoveIndicator(res1, res2);
}
}
}
}
// Function to show a single move indicator (original behavior)
myFunctions.showSingleMoveIndicator = function(res1, res2) {
// Check if player is playing as black
const isPlayingAsBlack = board.game.getPlayingAs() === 'black';
// Convert algebraic notation to numeric coordinates
// The conversion depends on whether we're playing as white or black
let fromSquare, toSquare;
if (isPlayingAsBlack) {
// Inverted mapping for black perspective
fromSquare = res1.replace(/^a/, "8")
.replace(/^b/, "7")
.replace(/^c/, "6")
.replace(/^d/, "5")
.replace(/^e/, "4")
.replace(/^f/, "3")
.replace(/^g/, "2")
.replace(/^h/, "1");
toSquare = res2.replace(/^a/, "8")
.replace(/^b/, "7")
.replace(/^c/, "6")
.replace(/^d/, "5")
.replace(/^e/, "4")
.replace(/^f/, "3")
.replace(/^g/, "2")
.replace(/^h/, "1");
} else {
// Standard mapping for white perspective
fromSquare = res1.replace(/^a/, "1")
.replace(/^b/, "2")
.replace(/^c/, "3")
.replace(/^d/, "4")
.replace(/^e/, "5")
.replace(/^f/, "6")
.replace(/^g/, "7")
.replace(/^h/, "8");
toSquare = res2.replace(/^a/, "1")
.replace(/^b/, "2")
.replace(/^c/, "3")
.replace(/^d/, "4")
.replace(/^e/, "5")
.replace(/^f/, "6")
.replace(/^g/, "7")
.replace(/^h/, "8");
}
// Use arrows or highlights based on user preference
if (myVars.moveIndicatorType === 'arrows') {
// Draw an arrow from the source to the destination square
// Pass the converted square coordinates to ensure consistency with highlights
myFunctions.drawArrow(fromSquare, toSquare, myVars.persistentHighlights);
} else {
// Use the original highlighting method
if (myVars.persistentHighlights) {
// Add highlights with custom class for easier removal later
$(board.nodeName)
.prepend('<div class="highlight square-' + toSquare + ' bro persistent-highlight" style="background-color: rgb(235, 97, 80); opacity: 0.71;" data-test-element="highlight"></div>');
$(board.nodeName)
.prepend('<div class="highlight square-' + fromSquare + ' bro persistent-highlight" style="background-color: rgb(235, 97, 80); opacity: 0.71;" data-test-element="highlight"></div>');
} else {
// Use the original temporary highlights
$(board.nodeName)
.prepend('<div class="highlight square-' + toSquare + ' bro" style="background-color: rgb(235, 97, 80); opacity: 0.71;" data-test-element="highlight"></div>')
.children(':first')
.delay(1800)
.queue(function() {
$(this)
.remove();
});
$(board.nodeName)
.prepend('<div class="highlight square-' + fromSquare + ' bro" style="background-color: rgb(235, 97, 80); opacity: 0.71;" data-test-element="highlight"></div>')
.children(':first')
.delay(1800)
.queue(function() {
$(this)
.remove();
});
}
}
}
// Function to show multiple move indicators with varying opacity or colors
myFunctions.showMultipleMoveIndicators = function() {
// Limit to the number of moves specified in settings
const movesToShow = Math.min(myVars.numberOfMovesToShow, myVars.topMoves.length);
console.log("Showing multiple moves:", movesToShow, "out of", myVars.topMoves.length, "available");
console.log("Using multicolor mode:", myVars.useMulticolorMoves);
// Get the best evaluation for normalization
const bestEval = myVars.topMoves[0].evaluation;
// Check if player is playing as black
const isPlayingAsBlack = board.game.getPlayingAs() === 'black';
// Show each move with opacity based on relative strength or different colors
for (let i = 0; i < movesToShow; i++) {
const moveInfo = myVars.topMoves[i];
const move = moveInfo.move;
// Skip if move is undefined
if (!move) continue;
const res1 = move.substring(0, 2);
const res2 = move.substring(2, 4);
// Variables for styling
let opacity = 0.9;
let moveColor = null;
if (myVars.useMulticolorMoves) {
// Use different colors for each move
// Get the color from settings, or use default if not set
moveColor = myVars.moveColors[i + 1] || getDefaultMoveColor(i);
// Use full opacity for multicolor mode
opacity = 0.9;
} else {
// Calculate opacity based on relative strength
// Best move gets 0.9 opacity, others get progressively lower
if (i > 0) {
// For non-mate positions, calculate relative strength
if (!moveInfo.isMate && !myVars.topMoves[0].isMate) {
// Calculate relative strength (0.0 to 1.0)
const relativeStrength = Math.max(0, 1 - Math.abs(bestEval - moveInfo.evaluation) / 3);
// Scale opacity from 0.3 to 0.9 based on strength
opacity = 0.3 + (relativeStrength * 0.6);
} else {
// For mate positions, use fixed opacity values
opacity = 0.9 - (i * 0.15);
}
}
// Ensure opacity is within reasonable bounds
opacity = Math.max(0.3, Math.min(0.9, opacity));
}
// Helper function to get default color for a move index
function getDefaultMoveColor(index) {
const defaultColors = [
'#F44336', // Red for best move
'#FF9800', // Orange for 2nd best
'#FFEB3B', // Yellow for 3rd best
'#4CAF50', // Green for 4th best
'#2196F3' // Blue for 5th best
];
return defaultColors[index] || '#9C27B0'; // Default to purple if out of range
}
// Convert algebraic notation to numeric coordinates
let fromSquare, toSquare;
if (isPlayingAsBlack) {
// Inverted mapping for black perspective
fromSquare = res1.replace(/^a/, "8")
.replace(/^b/, "7")
.replace(/^c/, "6")
.replace(/^d/, "5")
.replace(/^e/, "4")
.replace(/^f/, "3")
.replace(/^g/, "2")
.replace(/^h/, "1");
toSquare = res2.replace(/^a/, "8")
.replace(/^b/, "7")
.replace(/^c/, "6")
.replace(/^d/, "5")
.replace(/^e/, "4")
.replace(/^f/, "3")
.replace(/^g/, "2")
.replace(/^h/, "1");
} else {
// Standard mapping for white perspective
fromSquare = res1.replace(/^a/, "1")
.replace(/^b/, "2")
.replace(/^c/, "3")
.replace(/^d/, "4")
.replace(/^e/, "5")
.replace(/^f/, "6")
.replace(/^g/, "7")
.replace(/^h/, "8");
toSquare = res2.replace(/^a/, "1")
.replace(/^b/, "2")
.replace(/^c/, "3")
.replace(/^d/, "4")
.replace(/^e/, "5")
.replace(/^f/, "6")
.replace(/^g/, "7")
.replace(/^h/, "8");
}
// Get the color to use (either from multicolor settings or default)
let highlightColor = 'rgb(235, 97, 80)'; // Default red color
if (myVars.useMulticolorMoves) {
// Convert hex color to RGB for highlights
const moveColor = myVars.moveColors[i + 1] || getDefaultMoveColor(i);
highlightColor = hexToRgb(moveColor);
}
// Helper function to convert hex color to RGB format
function hexToRgb(hex) {
// Remove # if present
hex = hex.replace('#', '');
// Parse the hex values
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Return RGB format
return `rgb(${r}, ${g}, ${b})`;
}
// Use arrows or highlights based on user preference
if (myVars.moveIndicatorType === 'arrows') {
// Draw an arrow with the calculated opacity and color
myFunctions.drawArrow(fromSquare, toSquare, myVars.persistentHighlights, opacity, myVars.useMulticolorMoves ? myVars.moveColors[i + 1] : null);
} else {
// Use highlighting with the calculated opacity and color
if (myVars.persistentHighlights) {
// Add highlights with custom class for easier removal later
$(board.nodeName)
.prepend('<div class="highlight square-' + toSquare + ' bro persistent-highlight" style="background-color: ' + highlightColor + '; opacity: ' + opacity + ';" data-test-element="highlight"></div>');
$(board.nodeName)
.prepend('<div class="highlight square-' + fromSquare + ' bro persistent-highlight" style="background-color: ' + highlightColor + '; opacity: ' + opacity + ';" data-test-element="highlight"></div>');
} else {
// Use temporary highlights with the calculated opacity and color
$(board.nodeName)
.prepend('<div class="highlight square-' + toSquare + ' bro" style="background-color: ' + highlightColor + '; opacity: ' + opacity + ';" data-test-element="highlight"></div>')
.children(':first')
.delay(1800)
.queue(function() {
$(this)
.remove();
});
$(board.nodeName)
.prepend('<div class="highlight square-' + fromSquare + ' bro" style="background-color: ' + highlightColor + '; opacity: ' + opacity + ';" data-test-element="highlight"></div>')
.children(':first')
.delay(1800)
.queue(function() {
$(this)
.remove();
});
}
}
}
}
// Add a function to clear highlights
myFunctions.clearHighlights = function() {
// Remove all persistent highlights
$('.persistent-highlight').remove();
}
// Add a function to clear arrows
myFunctions.clearArrows = function() {
// Remove all arrows
$('.chess-arrow-svg').remove();
}
// Function to create and update the virtual chessboard
myFunctions.updateVirtualChessboard = function() {
const virtualChessboard = document.getElementById('virtualChessboard');
if (!virtualChessboard) return;
// Clear the virtual chessboard
virtualChessboard.innerHTML = '';
// Get the current FEN from the board
const fen = board.game.getFEN();
const fenParts = fen.split(' ');
const position = fenParts[0];
const rows = position.split('/');
// Create the chessboard squares
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const square = document.createElement('div');
square.className = 'virtual-square';
square.style.position = 'absolute';
square.style.width = '12.5%';
square.style.height = '12.5%';
square.style.top = (row * 12.5) + '%';
square.style.left = (col * 12.5) + '%';
square.style.backgroundColor = (row + col) % 2 === 0 ? '#f0d9b5' : '#b58863';
// Add coordinates
if (row === 7) {
const fileLabel = document.createElement('div');
fileLabel.style.position = 'absolute';
fileLabel.style.bottom = '2px';
fileLabel.style.right = '2px';
fileLabel.style.fontSize = '10px';
fileLabel.style.color = (row + col) % 2 === 0 ? '#b58863' : '#f0d9b5';
fileLabel.textContent = String.fromCharCode(97 + col); // 'a' to 'h'
square.appendChild(fileLabel);
}
if (col === 0) {
const rankLabel = document.createElement('div');
rankLabel.style.position = 'absolute';
rankLabel.style.top = '2px';
rankLabel.style.left = '2px';
rankLabel.style.fontSize = '10px';
rankLabel.style.color = (row + col) % 2 === 0 ? '#b58863' : '#f0d9b5';
rankLabel.textContent = 8 - row; // '8' to '1'
square.appendChild(rankLabel);
}
// Add data attributes for easier reference
const file = String.fromCharCode(97 + col); // 'a' to 'h'
const rank = 8 - row; // '8' to '1'
square.dataset.square = file + rank;
virtualChessboard.appendChild(square);
}
}
// Debug log the FEN string
console.log("Virtual chessboard FEN:", fen);
// Place pieces on the board
let rowIndex = 0;
for (const row of rows) {
let colIndex = 0;
for (let i = 0; i < row.length; i++) {
const char = row[i];
if (/[1-8]/.test(char)) {
// Skip empty squares
colIndex += parseInt(char);
} else {
// Place a piece
const pieceElement = document.createElement('div');
pieceElement.className = 'virtual-piece';
pieceElement.style.position = 'absolute';
pieceElement.style.width = '12.5%';
pieceElement.style.height = '12.5%';
pieceElement.style.top = (rowIndex * 12.5) + '%';
pieceElement.style.left = (colIndex * 12.5) + '%';
pieceElement.style.backgroundSize = 'contain';
pieceElement.style.backgroundRepeat = 'no-repeat';
pieceElement.style.backgroundPosition = 'center';
pieceElement.style.zIndex = '1';
// Set the piece image based on the FEN character
const pieceType = char.toLowerCase();
const pieceColor = char === char.toLowerCase() ? 'b' : 'w';
// Map FEN characters to piece types
const pieceMap = {
'p': 'pawn',
'r': 'rook',
'n': 'knight',
'b': 'bishop',
'q': 'queen',
'k': 'king'
};
// Debug log the piece being placed
console.log(`Placing ${pieceColor}${pieceMap[pieceType]} at row ${rowIndex}, col ${colIndex}`);
// Use chess.com piece style with direct URLs
const pieceUrls = {
'wp': 'https://www.chess.com/chess-themes/pieces/neo/150/wp.png',
'wn': 'https://www.chess.com/chess-themes/pieces/neo/150/wn.png',
'wb': 'https://www.chess.com/chess-themes/pieces/neo/150/wb.png',
'wr': 'https://www.chess.com/chess-themes/pieces/neo/150/wr.png',
'wq': 'https://www.chess.com/chess-themes/pieces/neo/150/wq.png',
'wk': 'https://www.chess.com/chess-themes/pieces/neo/150/wk.png',
'bp': 'https://www.chess.com/chess-themes/pieces/neo/150/bp.png',
'bn': 'https://www.chess.com/chess-themes/pieces/neo/150/bn.png',
'bb': 'https://www.chess.com/chess-themes/pieces/neo/150/bb.png',
'br': 'https://www.chess.com/chess-themes/pieces/neo/150/br.png',
'bq': 'https://www.chess.com/chess-themes/pieces/neo/150/bq.png',
'bk': 'https://www.chess.com/chess-themes/pieces/neo/150/bk.png'
};
const pieceKey = pieceColor + pieceType;
pieceElement.style.backgroundImage = `url(${pieceUrls[pieceKey]})`;
// Add a fallback in case the direct URL doesn't work
pieceElement.onerror = function() {
pieceElement.style.backgroundImage = `url(https://www.chess.com/chess-themes/pieces/neo/150/${pieceColor}${pieceMap[pieceType]}.png)`;
};
// Add a text fallback in case images don't load
pieceElement.textContent = char;
pieceElement.style.display = 'flex';
pieceElement.style.justifyContent = 'center';
pieceElement.style.alignItems = 'center';
pieceElement.style.fontSize = '20px';
pieceElement.style.fontWeight = 'bold';
pieceElement.style.color = pieceColor === 'w' ? '#fff' : '#000';
pieceElement.style.textShadow = pieceColor === 'w' ? '0 0 2px #000' : '0 0 2px #fff';
virtualChessboard.appendChild(pieceElement);
colIndex++;
}
}
rowIndex++;
}
}
// Function to show move indicators on the virtual chessboard
myFunctions.showVirtualMoveIndicator = function(fromSquare, toSquare, opacity = 0.7, color = 'rgb(235, 97, 80)') {
const virtualChessboard = document.getElementById('virtualChessboard');
if (!virtualChessboard) return;
// Convert numeric coordinates to algebraic notation if needed
let fromAlgebraic = fromSquare;
let toAlgebraic = toSquare;
// If fromSquare is numeric (e.g., "11", "22"), convert to algebraic (e.g., "a1", "b2")
if (/^\d+$/.test(fromSquare)) {
const file = String.fromCharCode(96 + parseInt(fromSquare[1])); // '1' -> 'a', '2' -> 'b', etc.
const rank = fromSquare[0];
fromAlgebraic = file + rank;
}
// Same for toSquare
if (/^\d+$/.test(toSquare)) {
const file = String.fromCharCode(96 + parseInt(toSquare[1])); // '1' -> 'a', '2' -> 'b', etc.
const rank = toSquare[0];
toAlgebraic = file + rank;
}
// Use arrows or highlights based on user preference
if (myVars.moveIndicatorType === 'arrows') {
// Draw an arrow from 'from' to 'to'
myFunctions.drawVirtualArrow(fromAlgebraic, toAlgebraic, opacity, color);
} else {
// Create highlight for the 'from' square
const fromHighlight = document.createElement('div');
fromHighlight.className = 'virtual-highlight';
fromHighlight.style.position = 'absolute';
fromHighlight.style.width = '12.5%';
fromHighlight.style.height = '12.5%';
fromHighlight.style.backgroundColor = color;
fromHighlight.style.opacity = opacity;
fromHighlight.style.zIndex = '2';
// Create highlight for the 'to' square
const toHighlight = document.createElement('div');
toHighlight.className = 'virtual-highlight';
toHighlight.style.position = 'absolute';
toHighlight.style.width = '12.5%';
toHighlight.style.height = '12.5%';
toHighlight.style.backgroundColor = color;
toHighlight.style.opacity = opacity;
toHighlight.style.zIndex = '2';
// Position the highlights based on the square coordinates
const squares = virtualChessboard.querySelectorAll('.virtual-square');
squares.forEach(square => {
if (square.dataset.square === fromAlgebraic) {
fromHighlight.style.top = square.style.top;
fromHighlight.style.left = square.style.left;
virtualChessboard.appendChild(fromHighlight);
}
if (square.dataset.square === toAlgebraic) {
toHighlight.style.top = square.style.top;
toHighlight.style.left = square.style.left;
virtualChessboard.appendChild(toHighlight);
}
});
}
}
// Function to draw an arrow on the virtual chessboard
myFunctions.drawVirtualArrow = function(fromSquare, toSquare, opacity = 0.7, color = 'rgb(235, 97, 80)') {
const virtualChessboard = document.getElementById('virtualChessboard');
if (!virtualChessboard) return;
// Find the positions of the squares
let fromPos = null;
let toPos = null;
const squares = virtualChessboard.querySelectorAll('.virtual-square');
squares.forEach(square => {
if (square.dataset.square === fromSquare) {
const rect = square.getBoundingClientRect();
const boardRect = virtualChessboard.getBoundingClientRect();
fromPos = {
x: (rect.left - boardRect.left) + (rect.width / 2),
y: (rect.top - boardRect.top) + (rect.height / 2)
};
}
if (square.dataset.square === toSquare) {
const rect = square.getBoundingClientRect();
const boardRect = virtualChessboard.getBoundingClientRect();
toPos = {
x: (rect.left - boardRect.left) + (rect.width / 2),
y: (rect.top - boardRect.top) + (rect.height / 2)
};
}
});
if (!fromPos || !toPos) return;
// Create SVG element for the arrow
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('class', 'virtual-arrow');
svg.style.position = 'absolute';
svg.style.top = '0';
svg.style.left = '0';
svg.style.zIndex = '3';
svg.style.pointerEvents = 'none';
// Calculate the angle and length of the arrow
const dx = toPos.x - fromPos.x;
const dy = toPos.y - fromPos.y;
const angle = Math.atan2(dy, dx);
const length = Math.sqrt(dx * dx + dy * dy);
// Adjust start and end points to not cover the pieces
const squareSize = virtualChessboard.getBoundingClientRect().width / 8;
const margin = squareSize * 0.3;
const startX = fromPos.x + Math.cos(angle) * margin;
const startY = fromPos.y + Math.sin(angle) * margin;
const endX = toPos.x - Math.cos(angle) * margin;
const endY = toPos.y - Math.sin(angle) * margin;
// Create the arrow shaft
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', startX);
line.setAttribute('y1', startY);
line.setAttribute('x2', endX);
line.setAttribute('y2', endY);
line.setAttribute('stroke', color);
line.setAttribute('stroke-width', squareSize / 8);
line.setAttribute('opacity', opacity);
// Create the arrow head
const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const arrowSize = squareSize / 3;
const arrowAngle = Math.PI / 7;
const point1X = endX;
const point1Y = endY;
const point2X = endX - arrowSize * Math.cos(angle - arrowAngle);
const point2Y = endY - arrowSize * Math.sin(angle - arrowAngle);
const point3X = endX - arrowSize * 0.6 * Math.cos(angle);
const point3Y = endY - arrowSize * 0.6 * Math.sin(angle);
const point4X = endX - arrowSize * Math.cos(angle + arrowAngle);
const point4Y = endY - arrowSize * Math.sin(angle + arrowAngle);
arrowHead.setAttribute('points', `${point1X},${point1Y} ${point2X},${point2Y} ${point3X},${point3Y} ${point4X},${point4Y}`);
arrowHead.setAttribute('fill', color);
arrowHead.setAttribute('opacity', opacity);
// Add elements to the SVG
svg.appendChild(line);
svg.appendChild(arrowHead);
// Add the SVG to the virtual chessboard
virtualChessboard.appendChild(svg);
}
// Function to clear virtual chessboard highlights and arrows
myFunctions.clearVirtualMoveIndicators = function() {
const virtualChessboard = document.getElementById('virtualChessboard');
if (!virtualChessboard) return;
// Remove all highlights
const highlights = virtualChessboard.querySelectorAll('.virtual-highlight');
highlights.forEach(highlight => highlight.remove());
// Remove all arrows
const arrows = virtualChessboard.querySelectorAll('.virtual-arrow');
arrows.forEach(arrow => arrow.remove());
}
// Observer instance for auto queue
myVars.newGameObserver = null;
// Function to check for and click the "New" button
myFunctions.clickNewGameButton = function() {
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
if (buttons[i].innerText.includes('New')) {
console.log('Auto Queue: Found "New" button, clicking it');
buttons[i].click();
return true;
}
}
return false;
}
// Function to start observing for new buttons
myFunctions.startNewGameObserver = function() {
// First try to click any existing button
myFunctions.clickNewGameButton();
// If observer already exists, disconnect it first
if (myVars.newGameObserver) {
myFunctions.stopNewGameObserver();
}
// Create a new observer
myVars.newGameObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length > 0) {
myFunctions.clickNewGameButton();
}
});
});
// Start observing the document body for changes
myVars.newGameObserver.observe(document.body, { childList: true, subtree: true });
console.log('Auto Queue: Started observing for New Game buttons');
}
// Function to stop observing
myFunctions.stopNewGameObserver = function() {
if (myVars.newGameObserver) {
myVars.newGameObserver.disconnect();
myVars.newGameObserver = null;
console.log('Auto Queue: Stopped observing for New Game buttons');
}
}
// Function to toggle the auto queue observer based on the current setting
myFunctions.updateAutoQueueObserver = function() {
if (myVars.autoQueue) {
myFunctions.startNewGameObserver();
} else {
myFunctions.stopNewGameObserver();
}
}
// Function to draw an arrow on the chess board
myFunctions.drawArrow = function(fromSquare, toSquare, isPersistent, customOpacity, customColor) {
// Store the move information for the server
if (!myVars.bestMove) {
myVars.bestMove = fromSquare + toSquare;
console.log('Setting best move for server:', myVars.bestMove);
}
// Always update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
console.log('Sending move to server:', fromSquare + toSquare);
myFunctions.sendServerUpdate();
}
// Check if we should show arrows on the main board
if (myVars.moveIndicatorLocation !== 'main' && myVars.moveIndicatorLocation !== 'both') {
return;
}
// Get the board element and its dimensions
const boardElement = $(board.nodeName)[0];
const boardRect = boardElement.getBoundingClientRect();
const squareSize = boardRect.width / 8;
// Use provided opacity or default
const arrowOpacity = customOpacity !== undefined ? customOpacity : 0.9;
// Use provided color or default
const arrowColor = customColor || myVars.arrowColor || "#0077CC";
// Create a temporary highlight to find the square position
// This is a reliable way to get the correct position regardless of board orientation
const tempFromHighlight = document.createElement('div');
tempFromHighlight.className = 'highlight square-' + fromSquare;
tempFromHighlight.style.opacity = '0';
const tempToHighlight = document.createElement('div');
tempToHighlight.className = 'highlight square-' + toSquare;
tempToHighlight.style.opacity = '0';
// Add to board temporarily
boardElement.appendChild(tempFromHighlight);
boardElement.appendChild(tempToHighlight);
// Get positions
const fromRect = tempFromHighlight.getBoundingClientRect();
const toRect = tempToHighlight.getBoundingClientRect();
// Remove temporary elements
boardElement.removeChild(tempFromHighlight);
boardElement.removeChild(tempToHighlight);
// Calculate center coordinates relative to the board
const fromX = fromRect.left - boardRect.left + fromRect.width / 2;
const fromY = fromRect.top - boardRect.top + fromRect.height / 2;
const toX = toRect.left - boardRect.left + toRect.width / 2;
const toY = toRect.top - boardRect.top + toRect.height / 2;
// Create SVG element for the arrow
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", boardRect.width);
svg.setAttribute("height", boardRect.height);
svg.setAttribute("class", "chess-arrow-svg");
svg.style.position = "absolute";
svg.style.top = "0";
svg.style.left = "0";
svg.style.pointerEvents = "none";
svg.style.zIndex = "100";
// Calculate the angle and length of the arrow
const dx = toX - fromX;
const dy = toY - fromY;
const angle = Math.atan2(dy, dx);
const length = Math.sqrt(dx * dx + dy * dy);
// Note: arrowColor is now defined at the top of the function
// Adjust start and end points to not cover the pieces
const margin = squareSize * 0.3;
const startX = fromX + Math.cos(angle) * margin;
const startY = fromY + Math.sin(angle) * margin;
const endX = toX - Math.cos(angle) * margin;
const endY = toY - Math.sin(angle) * margin;
// Create a group for the arrow (for easier animation)
const arrowGroup = document.createElementNS(svgNS, "g");
// Add a filter for drop shadow
const filterId = `arrow-shadow-${Date.now()}`;
const filter = document.createElementNS(svgNS, "filter");
filter.setAttribute("id", filterId);
filter.setAttribute("x", "-20%");
filter.setAttribute("y", "-20%");
filter.setAttribute("width", "140%");
filter.setAttribute("height", "140%");
const feGaussianBlur = document.createElementNS(svgNS, "feGaussianBlur");
feGaussianBlur.setAttribute("in", "SourceAlpha");
feGaussianBlur.setAttribute("stdDeviation", "2");
feGaussianBlur.setAttribute("result", "blur");
const feOffset = document.createElementNS(svgNS, "feOffset");
feOffset.setAttribute("in", "blur");
feOffset.setAttribute("dx", "1");
feOffset.setAttribute("dy", "1");
feOffset.setAttribute("result", "offsetBlur");
const feFlood = document.createElementNS(svgNS, "feFlood");
feFlood.setAttribute("flood-color", "rgba(0,0,0,0.5)");
feFlood.setAttribute("flood-opacity", "0.3");
feFlood.setAttribute("result", "color");
const feComposite = document.createElementNS(svgNS, "feComposite");
feComposite.setAttribute("in", "color");
feComposite.setAttribute("in2", "offsetBlur");
feComposite.setAttribute("operator", "in");
feComposite.setAttribute("result", "shadow");
const feMerge = document.createElementNS(svgNS, "feMerge");
const feMergeNode1 = document.createElementNS(svgNS, "feMergeNode");
feMergeNode1.setAttribute("in", "shadow");
const feMergeNode2 = document.createElementNS(svgNS, "feMergeNode");
feMergeNode2.setAttribute("in", "SourceGraphic");
feMerge.appendChild(feMergeNode1);
feMerge.appendChild(feMergeNode2);
filter.appendChild(feGaussianBlur);
filter.appendChild(feOffset);
filter.appendChild(feFlood);
filter.appendChild(feComposite);
filter.appendChild(feMerge);
svg.appendChild(filter);
// Apply the filter to the arrow group
arrowGroup.setAttribute("filter", `url(#${filterId})`);
// Create the arrow shaft using a path for better control
const path = document.createElementNS(svgNS, "path");
// Check if we should use curved or straight arrows
const arrowStyle = myVars.arrowStyle || 'curved';
let pathData;
let ctrlX, ctrlY;
if (arrowStyle === 'curved') {
// CURVED ARROW STYLE (Chess.com style)
// Calculate control points for a slight curve that adapts to the move direction
// Adjust curve factor based on move length - shorter moves get more curve
const baseCurveFactor = 0.1;
const lengthFactor = Math.min(1, 150 / length); // Normalize length factor
const curveFactor = baseCurveFactor * lengthFactor;
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
// Determine curve direction based on the move
// We'll use the board center as a reference point to decide curve direction
const boardCenterX = boardRect.width / 2;
const boardCenterY = boardRect.height / 2;
// Calculate vectors from board center to start and end points
const startVecX = startX - boardCenterX;
const startVecY = startY - boardCenterY;
const endVecX = endX - boardCenterX;
const endVecY = endY - boardCenterY;
// Calculate cross product to determine which side to curve
// This will make the curve direction adapt based on the move's position relative to board center
const crossProduct = startVecX * endVecY - startVecY * endVecX;
// Apply curve in appropriate direction based on cross product
const perpX = Math.sign(crossProduct) * -Math.sin(angle) * length * curveFactor;
const perpY = Math.sign(crossProduct) * Math.cos(angle) * length * curveFactor;
ctrlX = midX + perpX;
ctrlY = midY + perpY;
// Create a quadratic bezier curve path
pathData = `M ${startX},${startY} Q ${ctrlX},${ctrlY} ${endX},${endY}`;
} else {
// STRAIGHT ARROW STYLE (Classic style)
// Simple straight line from start to end
pathData = `M ${startX},${startY} L ${endX},${endY}`;
// For arrow head calculation later, we still need control points
// For straight lines, the control point is just the end point
ctrlX = endX;
ctrlY = endY;
}
path.setAttribute("d", pathData);
path.setAttribute("stroke", arrowColor);
path.setAttribute("stroke-width", squareSize / 9);
path.setAttribute("fill", "none");
path.setAttribute("opacity", arrowOpacity);
path.setAttribute("stroke-linecap", "round");
// Create the arrow head
const arrowHead = document.createElementNS(svgNS, "polygon");
// Calculate the direction at the end of the curve/line
// For a quadratic bezier, the tangent at the end point is from the control point to the end point
// For straight lines, this will just be the angle of the line
const endAngle = Math.atan2(endY - ctrlY, endX - ctrlX);
let point1X, point1Y, point2X, point2Y, point3X, point3Y, point4X, point4Y;
if (arrowStyle === 'curved') {
// CURVED ARROW STYLE (Chess.com style)
// Adjust arrow size based on square size for better proportions
const arrowSize = squareSize / 3.2;
// Chess.com style arrow head with sharper angle
const arrowAngle = Math.PI / 7;
// Create a more refined arrow head shape with smooth transitions
point1X = endX; // Tip of the arrow
point1Y = endY;
// Left wing of arrow head
point2X = endX - arrowSize * Math.cos(endAngle - arrowAngle);
point2Y = endY - arrowSize * Math.sin(endAngle - arrowAngle);
// Middle indentation (chess.com style)
const indentFactor = 0.65; // How far back the middle indent goes
point3X = endX - arrowSize * indentFactor * Math.cos(endAngle);
point3Y = endY - arrowSize * indentFactor * Math.sin(endAngle);
// Right wing of arrow head
point4X = endX - arrowSize * Math.cos(endAngle + arrowAngle);
point4Y = endY - arrowSize * Math.sin(endAngle + arrowAngle);
} else {
// STRAIGHT ARROW STYLE (Classic style)
// Simpler arrow head for straight arrows
const arrowSize = squareSize / 3.5;
const arrowAngle = Math.PI / 6; // Wider angle for classic style
point1X = endX; // Tip of the arrow
point1Y = endY;
// Left wing of arrow head
point2X = endX - arrowSize * Math.cos(endAngle - arrowAngle);
point2Y = endY - arrowSize * Math.sin(endAngle - arrowAngle);
// Right wing of arrow head
point4X = endX - arrowSize * Math.cos(endAngle + arrowAngle);
point4Y = endY - arrowSize * Math.sin(endAngle + arrowAngle);
// For straight arrows, we use a triangular head (no middle indentation)
point3X = point1X; // Not used for straight arrows
point3Y = point1Y; // Not used for straight arrows
}
// Set the polygon points based on arrow style
if (arrowStyle === 'curved') {
// Four-point polygon for curved arrows (with middle indentation)
arrowHead.setAttribute("points", `${point1X},${point1Y} ${point2X},${point2Y} ${point3X},${point3Y} ${point4X},${point4Y}`);
} else {
// Three-point polygon for straight arrows (triangular head)
arrowHead.setAttribute("points", `${point1X},${point1Y} ${point2X},${point2Y} ${point4X},${point4Y}`);
}
arrowHead.setAttribute("fill", arrowColor);
arrowHead.setAttribute("opacity", arrowOpacity);
// Add elements to the arrow group
arrowGroup.appendChild(path);
arrowGroup.appendChild(arrowHead);
// Add the arrow group to the SVG
svg.appendChild(arrowGroup);
// Add the SVG to the board
boardElement.appendChild(svg);
// Check if animations are enabled
const animationsEnabled = myVars.arrowAnimation !== undefined ? myVars.arrowAnimation : true;
// Add entrance animation only if enabled
if (animationsEnabled) {
if (typeof anime !== 'undefined') {
// If anime.js is available, use it for smooth animation
anime({
targets: path,
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 300,
delay: 0
});
anime({
targets: arrowHead,
opacity: [0, arrowOpacity],
scale: [0.5, 1],
easing: 'easeInOutSine',
duration: 300,
delay: 200
});
} else {
// Fallback animation using CSS
path.style.strokeDasharray = length;
path.style.strokeDashoffset = length;
path.style.animation = 'arrow-draw 0.3s ease-in-out forwards';
arrowHead.style.opacity = '0';
arrowHead.style.animation = 'arrow-fade-in 0.2s ease-in-out 0.2s forwards';
// Add the animation keyframes if they don't exist
if (!document.getElementById('arrow-animations')) {
const style = document.createElement('style');
style.id = 'arrow-animations';
style.textContent = `
@keyframes arrow-draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes arrow-fade-in {
to {
opacity: ${arrowOpacity};
}
}
`;
document.head.appendChild(style);
}
}
} else {
// If animations are disabled, just show the arrow immediately
path.style.opacity = arrowOpacity;
arrowHead.style.opacity = arrowOpacity;
}
// If not persistent, remove after delay
if (!isPersistent) {
setTimeout(() => {
if (svg.parentNode) {
// Check if animations are enabled
if (animationsEnabled) {
// Fade out animation
const fadeOut = () => {
arrowGroup.style.transition = 'opacity 0.3s ease-out';
arrowGroup.style.opacity = '0';
setTimeout(() => {
if (svg.parentNode) {
svg.parentNode.removeChild(svg);
}
}, 300);
};
fadeOut();
} else {
// If animations are disabled, just remove immediately
svg.parentNode.removeChild(svg);
}
}
}, 1800);
}
}
// Modify the movePiece function to clear highlights and arrows when a move is made
myFunctions.movePiece = function(from, to){
// Clear any existing highlights and arrows when a move is made
myFunctions.clearHighlights();
myFunctions.clearArrows();
for (var each=0;each<board.game.getLegalMoves().length;each++){
if(board.game.getLegalMoves()[each].from == from){
if(board.game.getLegalMoves()[each].to == to){
var move = board.game.getLegalMoves()[each];
board.game.move({
...move,
promotion: 'false',
animate: false,
userGenerated: true
});
}
}
}
}
function parser(e){
// Store alternative moves for human-like play and multiple move suggestions
if(e.data.includes('info') && e.data.includes('pv') && !e.data.includes('bestmove')) {
try {
// Extract the move from the principal variation (pv)
const parts = e.data.split(' ');
const pvIndex = parts.indexOf('pv');
const cpIndex = parts.indexOf('cp');
const mateIndex = parts.indexOf('mate');
const depthIndex = parts.indexOf('depth');
const multipvIndex = parts.indexOf('multipv');
// Debug the raw engine output to understand what we're getting
console.log("Engine output:", e.data);
if(pvIndex !== -1 && parts[pvIndex + 1]) {
const move = parts[pvIndex + 1];
// Get evaluation for this move
let evaluation = 0;
let isMate = false;
let mateIn = 0;
if (cpIndex !== -1 && parts[cpIndex + 1]) {
evaluation = parseInt(parts[cpIndex + 1]) / 100; // Convert centipawns to pawns
} else if (mateIndex !== -1 && parts[mateIndex + 1]) {
isMate = true;
mateIn = parseInt(parts[mateIndex + 1]);
// Use a large value for mate
evaluation = mateIn > 0 ? 20 : -20;
}
// Get depth for this move
let depth = 0;
if (depthIndex !== -1 && parts[depthIndex + 1]) {
depth = parseInt(parts[depthIndex + 1]);
}
// Get multipv index if available (for multiple move analysis)
let multipvValue = 1; // Default to 1 if not specified
if (multipvIndex !== -1 && parts[multipvIndex + 1]) {
multipvValue = parseInt(parts[multipvIndex + 1]);
}
// Initialize topMoves array if it doesn't exist
if (!myVars.topMoves) {
myVars.topMoves = [];
}
// Create move info object
const moveInfo = {
move: move,
evaluation: evaluation,
isMate: isMate,
mateIn: mateIn,
depth: depth,
multipv: multipvValue
};
// Check if this move is already in the list
const existingIndex = myVars.topMoves.findIndex(m => m.move === move);
if (existingIndex !== -1) {
// Update existing move info
myVars.topMoves[existingIndex] = moveInfo;
} else {
// Add new move info
myVars.topMoves.push(moveInfo);
}
// Sort moves by evaluation (best first)
myVars.topMoves.sort((a, b) => b.evaluation - a.evaluation);
// Keep only the top N moves
if (myVars.topMoves.length > 5) {
myVars.topMoves = myVars.topMoves.slice(0, 5);
}
// Debug info
console.log("Added/updated move in topMoves:", move, "Eval:", evaluation, "Total moves:", myVars.topMoves.length);
console.log("Current topMoves:", JSON.stringify(myVars.topMoves));
// Also store for human-like play (backward compatibility)
if(!myVars.alternativeMoves) {
myVars.alternativeMoves = [];
}
// Only add if not already in the list
if(!myVars.alternativeMoves.includes(move)) {
myVars.alternativeMoves.push(move);
}
}
} catch (err) {
console.log('Error parsing alternative move:', err);
}
}
if(e.data.includes('bestmove')){
const bestMove = e.data.split(' ')[1];
console.log('Best move:', bestMove);
console.log('Top moves before reset:', myVars.topMoves ? myVars.topMoves.length : 0);
// Store the best move for server updates
myVars.bestMove = bestMove;
// If human mode is active, simulate human play
if(myVars.humanMode && myVars.humanMode.active && myVars.alternativeMoves && myVars.alternativeMoves.length > 0) {
// Get the alternative moves (excluding the best move)
const alternatives = myVars.alternativeMoves.filter(move => move !== bestMove);
// Simulate human play with thinking time
const moveToPlay = bestMove; // Default to best move
// Simulate thinking time
const thinkingTime = Math.random() *
(myVars.humanMode.moveTime.max - myVars.humanMode.moveTime.min) +
myVars.humanMode.moveTime.min;
console.log(`Human mode: Thinking for ${thinkingTime.toFixed(1)} seconds...`);
// Delay the move to simulate thinking
setTimeout(() => {
// Select move based on human-like error rates
const selectedMove = simulateHumanPlay(bestMove, alternatives);
// Play the selected move
console.log(`Human mode: Playing ${selectedMove}`);
myFunctions.color(selectedMove);
// Reset alternative moves for next turn
myVars.alternativeMoves = [];
// Update auto run status if auto run is enabled
if (myVars.autoRun) {
myFunctions.updateAutoRunStatus('on');
}
}, thinkingTime * 1000);
// Clear the thinking flag immediately to prevent multiple calls
isThinking = false;
myVars.engineRunning = false;
} else {
// Normal engine play (no human simulation)
myFunctions.color(bestMove);
isThinking = false;
myVars.engineRunning = false;
// Update auto run status if auto run is enabled
if (myVars.autoRun) {
myFunctions.updateAutoRunStatus('on');
}
// Keep top moves for display on external board
// Only reset alternative moves
myVars.alternativeMoves = [];
console.log('Preserving top moves for external board:', myVars.topMoves);
}
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
}
// Parse evaluation information
if(e.data.includes('info') && e.data.includes('score cp')) {
try {
const parts = e.data.split(' ');
const cpIndex = parts.indexOf('cp');
if(cpIndex !== -1 && parts[cpIndex + 1]) {
const evalValue = parseInt(parts[cpIndex + 1]) / 100; // Convert centipawns to pawns
myVars.currentEvaluation = evalValue;
// Get depth information
const depthIndex = parts.indexOf('depth');
let currentDepth = '';
if(depthIndex !== -1 && parts[depthIndex + 1]) {
currentDepth = parts[depthIndex + 1];
}
// Update depth info in evaluation text
updateEvalBar(evalValue, null, currentDepth);
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
}
} catch (err) {
console.log('Error parsing evaluation:', err);
}
}
// Parse mate information
if(e.data.includes('info') && e.data.includes('score mate')) {
try {
const parts = e.data.split(' ');
const mateIndex = parts.indexOf('mate');
if(mateIndex !== -1 && parts[mateIndex + 1]) {
const movesToMate = parseInt(parts[mateIndex + 1]);
const evalText = movesToMate > 0 ? `Mate in ${movesToMate}` : `Mate in ${Math.abs(movesToMate)}`;
myVars.currentEvaluation = evalText; // Store mate text for history
// Get depth information
const depthIndex = parts.indexOf('depth');
let currentDepth = '';
if(depthIndex !== -1 && parts[depthIndex + 1]) {
currentDepth = parts[depthIndex + 1];
}
updateEvalBar(movesToMate > 0 ? 20 : -20, evalText, currentDepth); // Use a large value to show mate
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
}
} catch (err) {
console.log('Error parsing mate:', err);
}
}
}
// Function to update the evaluation bar (chess.com style)
function updateEvalBar(evalValue, mateText = null, depth = '') {
if(!evalBar || !evalText) return;
// Store the current evaluation for reference
myVars.currentEvaluation = evalValue;
// Clamp the visual representation between -5 and 5
const clampedEval = Math.max(-5, Math.min(5, evalValue));
const percentage = 50 + (clampedEval * 10); // Convert to percentage (0-100)
// Smoothly animate the height change
evalBar.style.height = `${percentage}%`;
// Update color based on who's winning with smoother gradients
let whiteColor = myVars.whiteAdvantageColor || '#4CAF50'; // White advantage
let blackColor = myVars.blackAdvantageColor || '#F44336'; // Black advantage
let neutralColor = '#9E9E9E'; // Grey for equal
// Determine the color based on advantage
let gradientColor;
let textColor;
if(evalValue > 0.2) {
// White advantage - stronger color for bigger advantage
const intensity = Math.min(1, Math.abs(evalValue) / 5); // Scale from 0 to 1 based on advantage
gradientColor = whiteColor;
textColor = whiteColor;
// Apply intensity to make stronger advantages more vibrant
evalBar.style.opacity = 0.7 + (intensity * 0.3);
} else if(evalValue < -0.2) {
// Black advantage - stronger color for bigger advantage
const intensity = Math.min(1, Math.abs(evalValue) / 5); // Scale from 0 to 1 based on advantage
gradientColor = blackColor;
textColor = blackColor;
// Apply intensity to make stronger advantages more vibrant
evalBar.style.opacity = 0.7 + (intensity * 0.3);
} else {
// Near equal position
gradientColor = neutralColor;
textColor = '#FFFFFF';
evalBar.style.opacity = 1;
}
// Create chess.com-like gradient effect with subtle patterns
evalBar.style.backgroundImage = `
linear-gradient(
to bottom,
${gradientColor}ee,
${gradientColor}
),
linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(255,255,255,0.05)),
linear-gradient(rgba(255,255,255,0.15) 1px, transparent 1px)
`;
evalBar.style.backgroundSize = '100% 100%, 100% 100%, 100% 5%';
// Add subtle shadow at the top edge for depth effect
evalBar.style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.15)';
// Update evaluation text with chess.com-like formatting
if(mateText) {
// Mate situation
const mateColor = evalValue > 0 ? whiteColor : blackColor;
evalText.innerHTML = `<span style="color: ${mateColor}">${mateText}</span>${depth ? `<br><span style="font-size: 10px; color: rgba(255,255,255,0.7)">d${depth}</span>` : ''}`;
evalText.style.backgroundColor = '#2a2a2a';
} else {
// Normal evaluation
const sign = evalValue > 0 ? '+' : '';
evalText.innerHTML = `<span style="color: ${textColor}">${sign}${Math.abs(evalValue).toFixed(1)}</span>${depth ? `<br><span style="font-size: 10px; color: rgba(255,255,255,0.7)">d${depth}</span>` : ''}`;
// Change background color based on advantage (subtle effect)
if(Math.abs(evalValue) > 3) {
// Strong advantage - highlight the evaluation text
evalText.style.backgroundColor = evalValue > 0 ? 'rgba(76, 175, 80, 0.2)' : 'rgba(244, 67, 54, 0.2)';
evalText.style.boxShadow = `0 2px 6px ${evalValue > 0 ? 'rgba(76, 175, 80, 0.3)' : 'rgba(244, 67, 54, 0.3)'}`;
} else {
// Normal advantage
evalText.style.backgroundColor = '#2a2a2a';
evalText.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
}
}
// Add visual pulse effect on significant changes
const previousEval = myVars.previousEvaluation || 0;
if(Math.abs(evalValue - previousEval) > 0.5) {
// Significant change detected - add pulse animation
evalBar.style.animation = 'none';
evalBar.offsetHeight; // Trigger reflow
evalBar.style.animation = 'evalPulse 0.5s ease-in-out';
}
// Store current evaluation for next comparison
myVars.previousEvaluation = evalValue;
}
// Add enhanced pulse animation
const pulseAnimation = document.createElement('style');
pulseAnimation.textContent = `
@keyframes evalPulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
`;
document.head.appendChild(pulseAnimation);
myFunctions.reloadChessEngine = function() {
console.log(`Reloading the chess engine!`);
engine.engine.terminate();
isThinking = false;
myFunctions.loadChessEngine();
}
myFunctions.loadChessEngine = async function() {
if (!stockfishObjectURL) {
const stockfishText = await GM.getResourceText('stockfish.js'); // Await the async function
stockfishObjectURL = URL.createObjectURL(new Blob([stockfishText], { type: 'application/javascript' }));
}
console.log(stockfishObjectURL);
if (stockfishObjectURL) {
engine.engine = new Worker(stockfishObjectURL);
engine.engine.onmessage = e => {
parser(e);
};
engine.engine.onerror = e => {
console.log("Worker Error: " + e);
};
engine.engine.postMessage('ucinewgame');
// Set MultiPV mode if multiple moves are enabled
if (myVars.showMultipleMoves) {
const multipvValue = myVars.numberOfMovesToShow || 3;
console.log("Initializing engine with MultiPV:", multipvValue);
engine.engine.postMessage(`setoption name MultiPV value ${multipvValue}`);
}
// Set ELO if specified
if (myVars.eloRating) {
setEngineElo(myVars.eloRating);
}
}
console.log('Loaded chess engine');
};
// Function to set engine ELO
function setEngineElo(elo) {
if(!engine.engine) return;
// Stockfish supports UCI_Elo option to limit playing strength
engine.engine.postMessage(`setoption name UCI_Elo value ${elo}`);
// Also set UCI_LimitStrength to true to enable ELO limiting
engine.engine.postMessage('setoption name UCI_LimitStrength value true');
// Set Skill Level based on ELO (0-20 scale)
// This helps ensure the engine plays more consistently with the ELO rating
let skillLevel = Math.max(0, Math.min(20, Math.floor((elo - 1000) / 100)));
engine.engine.postMessage(`setoption name Skill Level value ${skillLevel}`);
// Set appropriate depth limits based on ELO
// Lower ELO should use lower max depth to play more consistently
let maxDepth;
if (elo < 1200) {
maxDepth = 5; // Beginner level
} else if (elo < 1500) {
maxDepth = 8; // Intermediate level
} else if (elo < 1800) {
maxDepth = 12; // Advanced level
} else if (elo < 2100) {
maxDepth = 15; // Expert level
} else if (elo < 2400) {
maxDepth = 18; // Master level
} else {
maxDepth = 22; // Grandmaster level
}
// Store the max depth for this ELO
myVars.maxDepthForElo = maxDepth;
// Update the depth slider max value based on ELO
if ($('#depthSlider')[0]) {
// Only update the max if the current value is higher than the new max
if (parseInt($('#depthSlider')[0].value) > maxDepth) {
$('#depthSlider')[0].value = maxDepth;
$('#depthText')[0].innerHTML = "Current Depth: <strong>" + maxDepth + "</strong>";
}
// Update the slider's max attribute
$('#depthSlider')[0].max = maxDepth;
// Add a note about depth limitation
const depthNote = document.getElementById('depthNote');
if (depthNote) {
depthNote.textContent = `(Max depth ${maxDepth} for ELO ${elo})`;
} else if ($('#depthText')[0]) {
const note = document.createElement('span');
note.id = 'depthNote';
note.style = 'font-size: 12px; color: #666; margin-left: 5px;';
note.textContent = `(Max depth ${maxDepth} for ELO ${elo})`;
$('#depthText')[0].appendChild(note);
}
}
console.log(`Engine ELO set to ${elo} with max depth ${maxDepth} and skill level ${skillLevel}`);
}
// Function to set human-like play parameters
function setHumanMode(level) {
if(!engine.engine) return;
// Define human-like play characteristics based on level
let elo, moveTime, errorRate, blunderRate;
switch(level) {
case 'beginner':
elo = 800;
moveTime = { min: 1, max: 5 }; // Seconds
errorRate = 0.3; // 30% chance of suboptimal moves
blunderRate = 0.15; // 15% chance of blunders
break;
case 'casual':
elo = 1200;
moveTime = { min: 2, max: 8 };
errorRate = 0.2;
blunderRate = 0.1;
break;
case 'intermediate':
elo = 1600;
moveTime = { min: 3, max: 12 };
errorRate = 0.15;
blunderRate = 0.05;
break;
case 'advanced':
elo = 2000;
moveTime = { min: 5, max: 15 };
errorRate = 0.1;
blunderRate = 0.03;
break;
case 'expert':
elo = 2400;
moveTime = { min: 8, max: 20 };
errorRate = 0.05;
blunderRate = 0.01;
break;
default:
elo = 1600; // Default to intermediate
moveTime = { min: 3, max: 12 };
errorRate = 0.15;
blunderRate = 0.05;
}
// Store human mode settings
myVars.humanMode = {
active: true,
level: level,
elo: elo,
moveTime: moveTime,
errorRate: errorRate,
blunderRate: blunderRate
};
// Set the engine ELO
setEngineElo(elo);
// Update UI to reflect human mode
if ($('#humanModeLevel')[0]) {
$('#humanModeLevel')[0].textContent = level.charAt(0).toUpperCase() + level.slice(1);
}
// Update the human mode info in the UI
const humanModeInfo = document.getElementById('humanModeInfo');
if (humanModeInfo) {
humanModeInfo.textContent = `Playing like a ${level} human (ELO ~${elo})`;
}
console.log(`Human mode set to ${level} (ELO: ${elo}, Error rate: ${errorRate}, Blunder rate: ${blunderRate})`);
}
// Function to calculate thinking time based on board position
function calculateThinkingTime(boardState) {
// Example logic: You can customize this based on your evaluation of the board
const complexity = evaluateBoardComplexity(boardState); // Implement this function based on your needs
const minTime = 100; // Minimum thinking time in milliseconds
const maxTime = 2000; // Maximum thinking time in milliseconds
// Scale thinking time based on complexity (this is just an example)
return Math.min(maxTime, minTime + complexity * 100); // Adjust the scaling factor as needed
}
// Function to simulate human-like play
function simulateHumanPlay(bestMove, alternativeMoves, boardState) {
if (!myVars.humanMode || !myVars.humanMode.active) {
return bestMove; // Return the best move if human mode is not active
}
// Validate boardState
if (!Array.isArray(boardState) || boardState.length !== 8 || !boardState.every(row => Array.isArray(row) && row.length === 8)) {
console.error('Invalid boardState:', boardState);
return bestMove; // Return the best move if boardState is invalid
}
const { errorRate, blunderRate } = myVars.humanMode;
// Calculate thinking time based on the current board state
const thinkingTime = calculateThinkingTime(boardState);
// Function to select a move based on human-like error rates
const selectMove = () => {
const random = Math.random();
// Simulate a blunder (choosing a bad move)
if (random < blunderRate && alternativeMoves.length > 2) {
// Pick one of the worst moves
const worstMoves = alternativeMoves.slice(-2);
return worstMoves[Math.floor(Math.random() * worstMoves.length)];
}
// Otherwise, return the best move
return bestMove;
};
// Return a promise that resolves after the thinking time
return new Promise((resolve) => {
setTimeout(() => {
resolve(selectMove());
}, thinkingTime);
});
}
// Function to extract opponent's rating from the page
function extractOpponentRating() {
// Try to find the opponent's rating using the new selector
try {
// Try the new selector first
const ratingElement = document.querySelector("#board-layout-player-top .cc-user-rating-white");
if (ratingElement) {
const ratingText = ratingElement.textContent.trim();
const ratingMatch = ratingText.match(/\((\d+)\)/);
if (ratingMatch && ratingMatch[1]) {
const rating = parseInt(ratingMatch[1]);
if (!isNaN(rating)) {
console.log(`Opponent rating detected: ${rating}`);
return rating;
}
}
}
// Fallback to old selector if new one fails
const ratingElements = document.querySelectorAll('.user-tagline-rating');
if (ratingElements.length >= 2) {
// Find the element that doesn't match the player's username
const playerUsername = document.querySelector('.user-username-component')?.textContent.trim();
for (const element of ratingElements) {
const usernameElement = element.closest('.user-tagline')?.querySelector('.user-username-component');
if (usernameElement && usernameElement.textContent.trim() !== playerUsername) {
const rating = parseInt(element.textContent.trim());
if (!isNaN(rating)) {
console.log(`Opponent rating detected (fallback): ${rating}`);
return rating;
}
}
}
}
} catch (error) {
console.error('Error extracting opponent rating:', error);
}
return null;
}
// Function to update fusion mode status
myFunctions.updateFusionMode = function(enabled) {
const fusionModeStatus = document.getElementById('fusionModeStatus');
if (fusionModeStatus) {
fusionModeStatus.textContent = enabled ? 'On' : 'Off';
fusionModeStatus.style.color = enabled ? '#4CAF50' : '#666';
}
// Store previous ELO when enabling fusion mode
if (enabled && !myVars.fusionMode) {
myVars.previousEloRating = myVars.eloRating;
}
myVars.fusionMode = enabled;
if (enabled) {
// Start polling for opponent ELO changes
myVars.pollingInterval = setInterval(() => {
const currentOpponentRating = extractOpponentRating(); // Get the current opponent rating
if (currentOpponentRating !== myVars.previousOpponentRating) {
myVars.isNewGame = true; // Set new game flag
myVars.previousOpponentRating = currentOpponentRating; // Update previous opponent rating
// Update the ELO slider to match opponent rating
if ($('#eloSlider')[0]) {
// Clamp the rating to the slider's min/max values
const clampedRating = Math.max(1000, Math.min(3000, currentOpponentRating));
$('#eloSlider')[0].value = clampedRating;
$('#eloValue')[0].textContent = clampedRating;
myVars.eloRating = clampedRating;
// Set the engine ELO
setEngineElo(clampedRating);
}
}
}, 2000); // Check every 2 seconds
} else {
// Stop polling when fusion mode is disabled
clearInterval(myVars.pollingInterval);
// Restore previous ELO setting when disabling fusion mode
if (myVars.previousEloRating) {
if ($('#eloSlider')[0]) {
$('#eloSlider')[0].value = myVars.previousEloRating;
$('#eloValue')[0].textContent = myVars.previousEloRating;
myVars.eloRating = myVars.previousEloRating;
// Set the engine ELO back to previous value
setEngineElo(myVars.previousEloRating);
}
}
// Reset the opponent rating info text
const opponentRatingInfo = document.getElementById('opponentRatingInfo');
if (opponentRatingInfo) {
opponentRatingInfo.textContent = 'When enabled, the engine will play at the same rating as your opponent';
}
}
}
// Function to update human mode status
myFunctions.updateHumanMode = function(enabled) {
const humanModeStatus = document.getElementById('humanModeStatus');
if (humanModeStatus) {
humanModeStatus.textContent = enabled ? 'On' : 'Off';
humanModeStatus.style.color = enabled ? '#4CAF50' : '#666';
}
// Store previous ELO when enabling human mode
if (enabled && !myVars.humanMode?.active) {
myVars.previousEloRating = myVars.eloRating;
}
if (enabled) {
// Apply the selected human mode level
const level = $('#humanModeSelect').val() || 'intermediate';
setHumanMode(level);
} else {
// Disable human mode
if (myVars.humanMode) {
myVars.humanMode.active = false;
}
// Restore previous ELO setting when disabling human mode
if (myVars.previousEloRating) {
if ($('#eloSlider')[0]) {
$('#eloSlider')[0].value = myVars.previousEloRating;
$('#eloValue')[0].textContent = myVars.previousEloRating;
myVars.eloRating = myVars.previousEloRating;
// Set the engine ELO back to previous value
setEngineElo(myVars.previousEloRating);
}
}
// Reset the human mode info text
const humanModeInfo = document.getElementById('humanModeInfo');
if (humanModeInfo) {
humanModeInfo.textContent = 'When enabled, the engine will play like a human with realistic mistakes and timing';
}
}
}
// Function to update engine ELO from UI
myFunctions.updateEngineElo = function() {
const eloValue = parseInt($('#eloSlider')[0].value);
$('#eloValue')[0].textContent = eloValue;
myVars.eloRating = eloValue;
if(engine.engine) {
setEngineElo(eloValue);
}
// Update the depth slider if it exists
if ($('#depthSlider')[0] && myVars.maxDepthForElo !== undefined) {
// If current depth is higher than max allowed for this ELO, adjust it
if (parseInt($('#depthSlider')[0].value) > myVars.maxDepthForElo) {
$('#depthSlider')[0].value = myVars.maxDepthForElo;
$('#depthText')[0].innerHTML = "Current Depth: <strong>" + myVars.maxDepthForElo + "</strong>";
// Re-add the depth note
const depthNote = document.getElementById('depthNote');
if (depthNote && $('#depthText')[0]) {
$('#depthText')[0].appendChild(depthNote);
}
}
}
}
var lastValue = 11;
myFunctions.runChessEngine = function(depth){
// Use the depth from slider if no specific depth is provided
if (depth === undefined) {
depth = parseInt($('#depthSlider')[0].value);
}
// Ensure depth doesn't exceed the max for current ELO
if (myVars.maxDepthForElo !== undefined && depth > myVars.maxDepthForElo) {
depth = myVars.maxDepthForElo;
console.log(`Depth limited to ${depth} based on current ELO setting`);
}
// Reset topMoves array before starting a new analysis
myVars.topMoves = [];
console.log("Reset topMoves array before analysis");
// Set MultiPV mode if multiple moves are enabled
if (myVars.showMultipleMoves) {
const multipvValue = myVars.numberOfMovesToShow || 3;
console.log("Setting MultiPV to", multipvValue);
engine.engine.postMessage(`setoption name MultiPV value ${multipvValue}`);
} else {
// Reset to single PV mode
console.log("Setting MultiPV to 1 (single move mode)");
engine.engine.postMessage(`setoption name MultiPV value 1`);
}
//var fen = myFunctions.rescan();
var fen = board.game.getFEN();
engine.engine.postMessage(`position fen ${fen}`);
console.log('updated: ' + `position fen ${fen}`);
isThinking = true;
myVars.engineRunning = true; // Set engine running flag for server updates
engine.engine.postMessage(`go depth ${depth}`);
lastValue = depth;
// Update the depth text
if ($('#depthText')[0]) {
$('#depthText')[0].innerHTML = "Current Depth: <strong>" + depth + "</strong>";
// Re-add the depth note if it exists
const depthNote = document.getElementById('depthNote');
if (depthNote && $('#depthText')[0]) {
$('#depthText')[0].appendChild(depthNote);
}
}
// Update the slider value to match
if ($('#depthSlider')[0]) {
$('#depthSlider')[0].value = depth;
}
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
// Store the current FEN for server updates
myVars.chess = { fen: function() { return fen; } };
myVars.bestMove = ''; // Reset best move
myFunctions.sendServerUpdate();
}
}
myFunctions.autoRun = function(lstValue){
// Only run if it's the player's turn and not already thinking
if(board.game.getTurn() == board.game.getPlayingAs() && !isThinking){
console.log(`Auto running engine at depth ${lstValue}`);
myFunctions.updateAutoRunStatus('running');
myFunctions.runChessEngine(lstValue);
} else {
console.log("Auto run skipped - not player's turn or engine is already thinking");
if (myVars.autoRun) {
myFunctions.updateAutoRunStatus('waiting');
}
}
}
document.onkeydown = function(e) {
switch (e.keyCode) {
case 81:
myFunctions.runChessEngine(1);
break;
case 87:
myFunctions.runChessEngine(2);
break;
case 69:
myFunctions.runChessEngine(3);
break;
case 82:
myFunctions.runChessEngine(4);
break;
case 84:
myFunctions.runChessEngine(5);
break;
case 89:
myFunctions.runChessEngine(6);
break;
case 85:
myFunctions.runChessEngine(7);
break;
case 73:
myFunctions.runChessEngine(8);
break;
case 79:
myFunctions.runChessEngine(9);
break;
case 80:
myFunctions.runChessEngine(10);
break;
case 65:
myFunctions.runChessEngine(11);
break;
case 83:
myFunctions.runChessEngine(12);
break;
case 68:
myFunctions.runChessEngine(13);
break;
case 70:
myFunctions.runChessEngine(14);
break;
case 71:
myFunctions.runChessEngine(15);
break;
case 72:
myFunctions.runChessEngine(16);
break;
case 74:
myFunctions.runChessEngine(17);
break;
case 75:
myFunctions.runChessEngine(18);
break;
case 76:
myFunctions.runChessEngine(19);
break;
case 90:
myFunctions.runChessEngine(20);
break;
case 88:
myFunctions.runChessEngine(21);
break;
case 67:
myFunctions.runChessEngine(22);
break;
case 86:
myFunctions.runChessEngine(23);
break;
case 66:
myFunctions.runChessEngine(24);
break;
case 78:
myFunctions.runChessEngine(25);
break;
case 77:
myFunctions.runChessEngine(26);
break;
case 187:
myFunctions.runChessEngine(100);
break;
}
};
myFunctions.spinner = function() {
if(isThinking == true){
$('#overlay')[0].style.display = 'block';
}
if(isThinking == false) {
$('#overlay')[0].style.display = 'none';
}
}
let dynamicStyles = null;
function addAnimation(body) {
if (!dynamicStyles) {
dynamicStyles = document.createElement('style');
dynamicStyles.type = 'text/css';
document.head.appendChild(dynamicStyles);
}
dynamicStyles.sheet.insertRule(body, dynamicStyles.length);
}
var loaded = false;
myFunctions.loadEx = function(){
try{
var tmpStyle;
var tmpDiv;
board = $('chess-board')[0] || $('wc-chess-board')[0];
myVars.board = board;
// Create evaluation bar container with chess.com-like styling
var evalBarContainer = document.createElement('div');
evalBarContainer.id = 'evalBarContainer';
evalBarContainer.style = `
position: absolute;
left: -30px;
top: 0;
width: 24px;
height: 100%;
background-color: #2a2a2a;
border: none;
border-radius: 3px;
overflow: hidden;
z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
`;
// Create the actual evaluation bar with improved styling
evalBar = document.createElement('div');
evalBar.id = 'evalBar';
evalBar.style = `
position: absolute;
bottom: 0;
width: 100%;
height: 50%;
background-color: #9E9E9E;
transition: height 0.25s cubic-bezier(0.4, 0.0, 0.2, 1), background-color 0.25s ease;
background-image:
linear-gradient(to bottom, rgba(255,255,255,0.1), rgba(255,255,255,0.05)),
linear-gradient(rgba(255,255,255,0.15) 1px, transparent 1px);
background-size: 100% 100%, 100% 5%;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
`;
// Create a center line for the evaluation bar
const centerLine = document.createElement('div');
centerLine.style = `
position: absolute;
top: 50%;
width: 100%;
height: 1px;
background-color: rgba(255,255,255,0.3);
z-index: 1;
`;
// Create evaluation text with improved styling
evalText = document.createElement('div');
evalText.id = 'evalText';
evalText.style = `
position: absolute;
top: -30px;
left: -5px;
width: 34px;
text-align: center;
font-weight: bold;
font-size: 13px;
color: #fff;
background-color: #2a2a2a;
padding: 4px;
border-radius: 3px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
z-index: 101;
font-family: 'Roboto Mono', monospace;
transition: color 0.25s ease;
`;
evalText.textContent = '0.0';
// Create centipawn scale markers with improved styling
const scaleMarkers = document.createElement('div');
scaleMarkers.style = `
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
pointer-events: none;
font-family: 'Roboto Mono', monospace;
`;
// Add more precise scale markers (chess.com style)
const markerPositions = [
{ value: '+5', position: 0 }, // +5.0
{ value: '+3', position: 20 }, // +3.0
{ value: '+2', position: 30 }, // +2.0
{ value: '+1', position: 40 }, // +1.0
{ value: '0', position: 50 }, // 0.0
{ value: '-1', position: 60 }, // -1.0
{ value: '-2', position: 70 }, // -2.0
{ value: '-3', position: 80 }, // -3.0
{ value: '-5', position: 100 } // -5.0
];
markerPositions.forEach(marker => {
// Create tick mark line
const tick = document.createElement('div');
tick.style = `
position: absolute;
left: 0;
top: ${marker.position}%;
width: 100%;
height: 1px;
background-color: rgba(255,255,255,0.15);
transform: translateY(-50%);
`;
// Only add value labels for major ticks
if (['+5', '+3', '+1', '0', '-1', '-3', '-5'].includes(marker.value)) {
const label = document.createElement('span');
label.textContent = marker.value;
label.style = `
position: absolute;
right: -20px;
top: ${marker.position}%;
transform: translateY(-50%);
font-size: 9px;
color: rgba(255,255,255,0.6);
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
`;
scaleMarkers.appendChild(label);
}
scaleMarkers.appendChild(tick);
});
// Add elements to the DOM
evalBarContainer.appendChild(evalBar);
evalBarContainer.appendChild(centerLine);
evalBarContainer.appendChild(scaleMarkers);
board.parentElement.style.position = 'relative';
board.parentElement.appendChild(evalBarContainer);
board.parentElement.appendChild(evalText);
// Create main container with header
var div = document.createElement('div');
div.setAttribute('style','background-color:white; height:auto; border-radius: 12px; box-shadow: 0 6px 16px rgba(0,0,0,0.15); padding: 0; max-width: 300px; max-height: 90vh; overflow-y: auto; position: relative; font-family: "Segoe UI", Arial, sans-serif;');
div.setAttribute('id','settingsContainer');
// Create header with collapse button
var header = document.createElement('div');
header.style = `
background-color: #2196F3;
color: white;
padding: 12px 15px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
letter-spacing: 0.3px;
`;
header.innerHTML = `
<span style="font-weight: bold; font-size: 15px;">Chess AI Controls</span>
<span id="collapseBtn" style="transition: transform 0.3s;">▼</span>
`;
//div.appendChild(header);
async function createDraggableHeader(div) {
// Set initial positioning and z-index
div.style.position = 'fixed';
div.style.zIndex = '9999';
div.style.margin = '0';
div.style.padding = '0';
var header = document.createElement('div');
header.style = `
background-color: #2196F3;
color: white;
padding: 12px 15px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
letter-spacing: 0.3px;
user-select: none;
`;
header.innerHTML = `
<span id="dragArea" style="font-weight: bold; font-size: 15px; flex-grow: 1; cursor: move;">Chess AI Controls</span>
<span id="collapseBtn" style="transition: transform 0.3s;">▼</span>
`;
div.appendChild(header);
// Make the entire div draggable
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
// Restore previous position on load
try {
const savedPosition = await GM.getValue('GUI Position', null);
if (savedPosition) {
xOffset = savedPosition.x;
yOffset = savedPosition.y;
div.style.transform = `translate3d(${xOffset}px, ${yOffset}px, 0)`;
}
} catch (error) {
console.error('Error loading saved position:', error);
}
// Drag area now includes the entire text span
const dragArea = header.querySelector('#dragArea');
// Event listeners for dragging
dragArea.addEventListener('mousedown', dragStart);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('mousemove', drag);
function dragStart(e) {
// Prevent default to stop text selection and scrolling
e.preventDefault();
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
}
function dragEnd(e) {
// Prevent default to stop any browser scrolling behavior
e.preventDefault();
initialX = currentX;
initialY = currentY;
isDragging = false;
// Save the current position
try {
GM.setValue('GUI Position', { x: xOffset, y: yOffset });
} catch (error) {
console.error('Error saving position:', error);
}
}
function drag(e) {
if (isDragging) {
// Prevent default to stop scrolling and text selection
e.preventDefault();
// Constrain to viewport
currentX = Math.max(0, Math.min(e.clientX - initialX, window.innerWidth - div.offsetWidth));
currentY = Math.max(0, Math.min(e.clientY - initialY, window.innerHeight - div.offsetHeight));
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, div);
}
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
// After setting position, check if container is too tall for the screen
// and adjust position if needed
setTimeout(() => {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight;
// If the container extends beyond the bottom of the screen
if (rect.bottom > windowHeight) {
// Calculate how much we need to move it up
const adjustment = Math.min(yPos, rect.bottom - windowHeight + 20);
if (adjustment > 0) {
yOffset = yPos - adjustment;
el.style.transform = `translate3d(${xPos}px, ${yOffset}px, 0)`;
// Save the adjusted position
try {
GM.setValue('GUI Position', { x: xOffset, y: yOffset });
} catch (error) {
console.error('Error saving adjusted position:', error);
}
}
}
}, 100);
}
return header;
}
// Usage example:
(async () => {
//var div = document.createElement('div');
await createDraggableHeader(div);
})();
// Create content container
var contentContainer = document.createElement('div');
contentContainer.id = 'aiControlsContent';
contentContainer.style = 'padding: 15px; font-family: "Segoe UI", Arial, sans-serif; font-size: 14px; overflow-x: hidden;';
// Add CSS for tabs
var tabStyle = document.createElement('style');
tabStyle.textContent = `
.tab-container {
width: 100%;
}
.tab-nav {
display: flex;
border-bottom: 2px solid #2196F3;
margin-bottom: 15px;
overflow-x: auto; /* Allow horizontal scrolling if needed */
flex-wrap: nowrap; /* Keep tabs in a single row */
justify-content: space-between; /* Distribute space evenly */
scrollbar-width: thin; /* For Firefox */
-ms-overflow-style: none; /* For IE and Edge */
}
.tab-nav::-webkit-scrollbar {
height: 4px; /* Small scrollbar for webkit browsers */
}
.tab-nav::-webkit-scrollbar-thumb {
background-color: rgba(33, 150, 243, 0.3);
border-radius: 4px;
}
.tab-button {
padding: 8px 5px; /* Reduce padding to fit all tabs */
background-color: #f8f8f8;
border: none;
border-radius: 8px 8px 0 0;
margin-right: 1px; /* Reduce margin between tabs */
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
color: #666;
flex: 1;
min-width: 60px; /* Ensure minimum width for readability */
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 -2px 5px rgba(0,0,0,0.05);
font-size: 12px; /* Reduce font size to fit better */
white-space: nowrap; /* Prevent text wrapping */
}
.tab-button:hover {
background-color: #e9f5ff;
color: #2196F3;
transform: translateY(-2px);
}
.tab-button.active {
background-color: #2196F3;
color: white;
box-shadow: 0 -2px 5px rgba(33,150,243,0.3);
transform: translateY(-3px);
position: relative;
}
.tab-button.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #2196F3;
}
.tab-content {
display: none;
padding: 10px 0;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Responsive design for different screen sizes */
@media (max-width: 500px) {
.tab-button {
padding: 8px 5px;
font-size: 11px;
min-width: 50px;
}
}
/* Adjust container for different screen heights */
@media (max-height: 800px) {
#settingsContainer {
max-height: 80vh !important;
}
}
@media (max-height: 600px) {
#settingsContainer {
max-height: 70vh !important;
}
}
/* Ensure content fits on very small screens */
@media (max-height: 500px) {
#settingsContainer {
max-height: 60vh !important;
}
.tab-content {
padding: 5px 0;
}
input[type="range"] {
height: 6px;
}
}
/* Toggle switch styles */
.switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .3s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 2px #2196F3;
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Button styles */
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
button:active {
transform: translateY(0);
}
/* Input styles */
input[type="range"] {
-webkit-appearance: none;
height: 8px;
border-radius: 4px;
background: #e0e0e0;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #2196F3;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #2196F3;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Select styles */
select {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 30px !important;
transition: all 0.2s;
}
select:focus {
border-color: #2196F3;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
/* Tooltip styles */
[title] {
position: relative;
}
[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
white-space: nowrap;
z-index: 1000;
font-size: 12px;
}
`;
document.head.appendChild(tabStyle);
var content = `<div style="margin: 0;">
<!-- Tab Navigation -->
<div class="tab-container">
<div class="tab-nav">
<button class="tab-button active" data-tab="engine">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 3px;">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
Engine
</button>
<button class="tab-button" data-tab="actions">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 3px;">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
Actions
</button>
<button class="tab-button" data-tab="visual">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 3px;">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
Visual
</button>
<button class="tab-button" data-tab="playstyle">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 3px;">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
Play
</button>
<button class="tab-button" data-tab="auto">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 3px;">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5"></path>
<path d="M2 12l10 5 10-5"></path>
</svg>
Auto
</button>
</div>
<!-- Engine Tab -->
<div id="engine-tab" class="tab-content active">
<div style="margin-bottom: 15px;">
<p id="depthText" style="margin: 0 0 5px 0;">Current Depth: <strong>11</strong></p>
<div style="display: flex; align-items: center;">
<div style="flex-grow: 1;">
<label for="depthSlider" style="display: block; margin-bottom: 5px;">Adjust Depth (1-30):</label>
<input type="range" id="depthSlider" name="depthSlider" min="1" max="30" step="1" value="11"
oninput="document.getElementById('depthText').innerHTML = 'Current Depth: <strong>' + this.value + '</strong>';"
style="width: 100%;" title="Higher depth = stronger analysis but slower calculation">
</div>
<button id="applyDepth" style="margin-left: 10px; padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);" title="Apply the selected depth to the engine">Apply</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<label for="eloSlider" style="margin-right: 5px;">Engine ELO Rating: <span id="eloValue">1500</span></label>
<button id="eloInfoBtn" title="ELO rating determines the playing strength of the engine" style="margin-left: 5px; padding: 0 5px; background-color: #2196F3; color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 12px;">?</button>
</div>
<input type="range" id="eloSlider" name="eloSlider" min="1000" max="3000" step="50" value="1500"
oninput="document.myFunctions.updateEngineElo()" style="width: 100%;">
<div id="eloDepthInfo" style="font-size: 12px; color: #666; margin-top: 5px; font-style: italic;">
Note: Lower ELO settings will limit the maximum search depth
</div>
</div>
</div>
<!-- Play Style Tab -->
<div id="playstyle-tab" class="tab-content">
<div style="display: flex; flex-direction: column; gap: 15px;">
<!-- Fusion Mode -->
<div style="border-left: 3px solid #2196F3; padding-left: 10px;">
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<label for="fusionModeToggle" style="margin-right: 10px; font-weight: bold;">Fusion Mode:</label>
<label class="switch">
<input type="checkbox" id="fusionMode" name="fusionMode" value="false">
<span class="slider"></span>
</label>
<span id="fusionModeStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div id="opponentRatingInfo" style="font-size: 12px; color: #666; margin-top: 5px;">
When enabled, the engine will match your opponent's rating
</div>
</div>
<!-- Human Mode -->
<div style="border-left: 3px solid #9C27B0; padding-left: 10px;">
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<label for="humanModeToggle" style="margin-right: 10px; font-weight: bold;">Human Mode:</label>
<label class="switch">
<input type="checkbox" id="humanMode" name="humanMode" value="false">
<span class="slider"></span>
</label>
<span id="humanModeStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="margin-top: 10px;">
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<label for="humanModeSelect" style="margin-right: 5px;">Human Skill Level: <span id="humanModeLevel">Intermediate</span></label>
<button id="humanModeInfoBtn" title="Choose how the engine mimics human play" style="margin-left: 5px; padding: 0 5px; background-color: #9C27B0; color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 12px;">?</button>
</div>
<select id="humanModeSelect" style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ddd;">
<option value="beginner">Beginner (ELO ~800)</option>
<option value="casual">Casual (ELO ~1200)</option>
<option value="intermediate" selected>Intermediate (ELO ~1600)</option>
<option value="advanced">Advanced (ELO ~2000)</option>
<option value="expert">Expert (ELO ~2400)</option>
</select>
</div>
<div id="humanModeInfo" style="font-size: 12px; color: #666; margin-top: 5px; font-style: italic;">
When enabled, the engine will play like a human with realistic mistakes and timing
</div>
</div>
</div>
</div>
<!-- Visual Settings Tab -->
<div id="visual-tab" class="tab-content">
<div style="margin-bottom: 15px;">
<label for="evalBarColor" style="display: block; margin-bottom: 5px;">Evaluation Bar Color Theme:</label>
<select id="evalBarColor" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd;">
<option value="default">Default (Green/Red)</option>
<option value="blue">Blue/Orange</option>
<option value="purple">Purple/Yellow</option>
<option value="custom">Custom</option>
</select>
</div>
<div id="customColorContainer" style="display: none; margin-bottom: 15px; padding: 10px; border: 1px dashed #ccc; border-radius: 4px;">
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<label for="whiteAdvantageColor">White Advantage:</label>
<input type="color" id="whiteAdvantageColor" value="#4CAF50" style="width: 40px; height: 30px;">
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<label for="blackAdvantageColor">Black Advantage:</label>
<input type="color" id="blackAdvantageColor" value="#F44336" style="width: 40px; height: 30px;">
</div>
</div>
<div style="margin-bottom: 15px;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input type="checkbox" id="showArrows" name="showArrows" value="true" checked style="margin-right: 8px;">
<label for="showArrows"> Show move arrows</label>
</div>
<div style="display: flex; align-items: center; margin-bottom: 12px;">
<input type="checkbox" id="persistentHighlights" name="persistentHighlights" value="true" checked style="margin-right: 8px;">
<label for="persistentHighlights"> Keep highlights until next move</label>
</div>
<div style="display: flex; align-items: center; margin-bottom: 12px; border-top: 1px solid #eee; padding-top: 10px;">
<input type="checkbox" id="useVirtualChessboard" name="useVirtualChessboard" value="false" style="margin-right: 8px;">
<label for="useVirtualChessboard"> Use virtual chessboard for move suggestions</label>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; margin-bottom: 15px; font-style: italic;">
Displays move suggestions on a virtual chessboard in the Actions tab instead of overlaying them on the main board (helps avoid detection)
</div>
<div style="display: flex; align-items: center; margin-bottom: 12px; border-top: 1px solid #eee; padding-top: 10px;">
<input type="checkbox" id="useExternalWindow" name="useExternalWindow" value="false" style="margin-right: 8px;">
<label for="useExternalWindow"> Open GUI in external window</label>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px; font-style: italic;">
Opens the Chess AI controls in a separate window or tab (requires local Python server)
<br><a href="#" id="downloadServerLink" style="color: #2196F3; text-decoration: underline;">Download the chess_ai_server.py file here</a>
</div>
<div id="externalWindowOptions" style="display: none; margin-top: 10px; padding: 10px; background-color: #f8f8f8; border-radius: 4px;">
<button id="startServerBtn" style="padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">Start Local Server</button>
<button id="openExternalWindowBtn" style="padding: 8px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Open External Window</button>
<div id="serverStatus" style="margin-top: 8px; font-size: 12px; color: #666;">
Server Status: <span id="serverStatusText">Not Running</span>
</div>
<div style="margin-top: 15px; border-top: 1px solid #ddd; padding-top: 10px;">
<div style="font-weight: bold; margin-bottom: 8px;">Move Indicator Location:</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<label style="display: flex; align-items: center;">
<input type="radio" name="moveIndicatorLocation" value="main" checked style="margin-right: 8px;">
Show on main board only
</label>
<label style="display: flex; align-items: center;">
<input type="radio" name="moveIndicatorLocation" value="external" style="margin-right: 8px;">
Show on external board only
</label>
<label style="display: flex; align-items: center;">
<input type="radio" name="moveIndicatorLocation" value="both" style="margin-right: 8px;">
Show on both boards
</label>
</div>
</div>
</div>
<div style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold;">Move Indicator Style:</label>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input type="radio" id="moveIndicatorHighlights" name="moveIndicatorType" value="highlights" checked style="margin-right: 8px;">
<label for="moveIndicatorHighlights"> Highlights</label>
</div>
<div style="display: flex; align-items: center;">
<input type="radio" id="moveIndicatorArrows" name="moveIndicatorType" value="arrows" style="margin-right: 8px;">
<label for="moveIndicatorArrows"> Arrows</label>
</div>
</div>
<!-- Arrow Color Customization -->
<div id="arrowCustomizationContainer" style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; display: none;">
<div id="arrowCustomizationSection">
<label style="display: block; margin-bottom: 8px; font-weight: bold;">Arrow Customization:</label>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
<label for="arrowColor">Arrow Color:</label>
<input type="color" id="arrowColor" value="#0077CC" style="width: 40px; height: 30px;">
<span style="font-size: 12px; color: #666; margin-left: 5px;">(Chess.com style)</span>
</div>
<!-- Arrow Style Options -->
<div style="margin-top: 12px; margin-bottom: 10px;">
<label style="display: block; margin-bottom: 8px;">Arrow Style:</label>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input type="radio" id="arrowStyleCurved" name="arrowStyle" value="curved" checked style="margin-right: 8px;">
<label for="arrowStyleCurved"> Curved arrows (Chess.com style)</label>
</div>
<div style="display: flex; align-items: center;">
<input type="radio" id="arrowStyleStraight" name="arrowStyle" value="straight" style="margin-right: 8px;">
<label for="arrowStyleStraight"> Straight arrows (Classic style)</label>
</div>
</div>
<div style="font-size: 12px; color: #666; margin-top: 15px; font-style: italic;">
Customize the color and style of the move arrows
</div>
</div><!-- End of arrowCustomizationSection -->
</div>
<!-- Arrow Animation Toggle (Separate from arrow customization) -->
<div id="arrowAnimationContainer" style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; display: none;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<label for="arrowAnimation" style="font-weight: bold;">Arrow Animation:</label>
<label class="switch" style="margin-left: 10px;">
<input type="checkbox" id="arrowAnimation" checked>
<span class="slider round"></span>
</label>
</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
Enable or disable the arrow drawing animation
</div>
</div>
<!-- Multiple Move Suggestions -->
<div style="margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<label for="showMultipleMoves" style="font-weight: bold;">Show Multiple Moves:</label>
<label class="switch" style="margin-left: 10px;">
<input type="checkbox" id="showMultipleMoves">
<span class="slider round"></span>
</label>
<span id="showMultipleMovesStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 15px;">
Show top 3-5 moves instead of just the best move
</div>
<div id="multipleMovesOptions" style="display: none; margin-top: 10px; padding: 10px; background-color: #f8f8f8; border-radius: 4px;">
<label for="numberOfMovesToShow" style="display: block; margin-bottom: 8px;">Number of moves to show:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="range" id="numberOfMovesToShow" min="2" max="5" value="3" style="flex: 1;">
<span id="numberOfMovesValue" style="min-width: 20px; text-align: center;">3</span>
</div>
<!-- Multicolor option -->
<div style="margin-top: 15px; border-top: 1px solid #ddd; padding-top: 15px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<label for="useMulticolorMoves" style="font-weight: bold;">Use different colors:</label>
<label class="switch" style="margin-left: 10px;">
<input type="checkbox" id="useMulticolorMoves">
<span class="slider round"></span>
</label>
<span id="useMulticolorMovesStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 15px;">
Use different colors for each move instead of varying opacity
</div>
<!-- Color pickers for each move -->
<div id="moveColorOptions" style="display: none; margin-top: 10px;">
<div style="display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;">
<label for="moveColor1" style="font-size: 12px;">Best move:</label>
<input type="color" id="moveColor1" value="#F44336" style="width: 100%;">
<label for="moveColor2" style="font-size: 12px;">2nd best:</label>
<input type="color" id="moveColor2" value="#FF9800" style="width: 100%;">
<label for="moveColor3" style="font-size: 12px;">3rd best:</label>
<input type="color" id="moveColor3" value="#FFEB3B" style="width: 100%;">
<label for="moveColor4" style="font-size: 12px;">4th best:</label>
<input type="color" id="moveColor4" value="#4CAF50" style="width: 100%;">
<label for="moveColor5" style="font-size: 12px;">5th best:</label>
<input type="color" id="moveColor5" value="#2196F3" style="width: 100%;">
</div>
</div>
</div>
<div id="opacityNote" style="font-size: 12px; color: #666; margin-top: 8px; font-style: italic;">
Opacity of move indicators will reflect move strength
</div>
</div>
</div>
</div>
</div>
<!-- Automation Tab -->
<div id="auto-tab" class="tab-content">
<div style="margin-bottom: 20px; border-left: 3px solid #FF9800; padding-left: 12px;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<label for="autoRunToggle" style="margin-right: 10px; font-weight: bold; color: #FF9800;">Auto Run:</label>
<label class="switch">
<input type="checkbox" id="autoRun" name="autoRun" value="false">
<span class="slider" style="background-color: #ccc;"></span>
</label>
<span id="autoRunStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
Automatically runs the engine when it's your turn
</div>
</div>
<div style="margin-bottom: 20px; border-left: 3px solid #4CAF50; padding-left: 12px;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<label for="autoMove" style="margin-right: 10px; font-weight: bold; color: #4CAF50;">Auto Move:</label>
<label class="switch">
<input type="checkbox" id="autoMove" name="autoMove" value="false">
<span class="slider" style="background-color: #ccc;"></span>
</label>
<span id="autoMoveStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
Automatically plays the best move for you
</div>
</div>
<div style="margin-bottom: 20px; border-left: 3px solid #9C27B0; padding-left: 12px;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<label for="autoQueue" style="margin-right: 10px; font-weight: bold; color: #9C27B0;">Auto Queue:</label>
<label class="switch">
<input type="checkbox" id="autoQueue" name="autoQueue" value="false">
<span class="slider" style="background-color: #ccc;"></span>
</label>
<span id="autoQueueStatus" style="margin-left: 10px; font-size: 12px; color: #666;">Off</span>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
Automatically clicks "New Game" button when a game ends
</div>
</div>
<div style="margin-top: 15px; background-color: #f8f8f8; padding: 12px; border-radius: 6px;">
<label style="display: block; margin-bottom: 10px; font-weight: bold;">Auto Run Delay (Seconds):</label>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="flex: 1;">
<label for="timeDelayMin" style="display: block; font-size: 12px; margin-bottom: 3px;">Minimum:</label>
<input type="number" id="timeDelayMin" name="timeDelayMin" min="0.1" value="0.1" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd;">
</div>
<span style="color: #666;">to</span>
<div style="flex: 1;">
<label for="timeDelayMax" style="display: block; font-size: 12px; margin-bottom: 3px;">Maximum:</label>
<input type="number" id="timeDelayMax" name="timeDelayMax" min="0.1" value="1" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd;">
</div>
</div>
<div style="font-size: 12px; color: #666; margin-top: 8px; font-style: italic;">
Random delay between min and max to simulate human thinking time
</div>
</div>
</div>
</div>
<!-- Actions Tab -->
<div id="actions-tab" class="tab-content">
<!-- Virtual Chessboard (only shown when enabled) -->
<div id="virtualChessboardContainer" style="display: none; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; padding: 10px; background-color: #f9f9f9;">
<div style="font-weight: bold; margin-bottom: 8px; color: #2196F3;">Virtual Chessboard</div>
<div id="virtualChessboard" style="width: 100%; aspect-ratio: 1; position: relative; margin-bottom: 10px; border: 1px solid #ccc; background-color: #fff;"></div>
<div style="font-size: 12px; color: #666; text-align: center;">
Move suggestions are shown here instead of on the main board
</div>
</div>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<button id="runEngineBtn" style="flex: 1; padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(76, 175, 80, 0.3);">
<span style="display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 5px;">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
Run Engine
</span>
</button>
<button id="stopEngineBtn" style="flex: 1; padding: 10px; background-color: #F44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(244, 67, 54, 0.3);">
<span style="display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 5px;">
<rect x="6" y="6" width="12" height="12"></rect>
</svg>
Stop Engine
</span>
</button>
</div>
<button id="saveSettingsBtn" style="width: 100%; padding: 10px; background-color: #2196F3; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; margin-bottom: 10px; box-shadow: 0 2px 5px rgba(33, 150, 243, 0.3);">
<span style="display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 5px;">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save Settings
</span>
</button>
<button id="showKeyboardShortcuts" style="width: 100%; padding: 10px; background-color: #9C27B0; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(156, 39, 176, 0.3); margin-bottom: 15px;">
<span style="display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 5px;">
<rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
<line x1="6" y1="8" x2="6" y2="8"></line>
<line x1="10" y1="8" x2="10" y2="8"></line>
<line x1="14" y1="8" x2="14" y2="8"></line>
<line x1="18" y1="8" x2="18" y2="8"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
<line x1="6" y1="16" x2="6" y2="16"></line>
<line x1="18" y1="16" x2="18" y2="16"></line>
<line x1="10" y1="16" x2="14" y2="16"></line>
</svg>
Keyboard Shortcuts
</span>
</button>
<!-- Engine Move History Section -->
<div id="moveHistoryContainer" style="margin-top: 15px; border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-height: 200px; overflow-y: auto;">
<h3 style="margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #333;">Engine Move History</h3>
<table id="moveHistoryTable" style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align: left; padding: 5px; border-bottom: 1px solid #ddd;">Move</th>
<th style="text-align: left; padding: 5px; border-bottom: 1px solid #ddd;">Eval</th>
<th style="text-align: left; padding: 5px; border-bottom: 1px solid #ddd;">Depth</th>
</tr>
</thead>
<tbody id="moveHistoryTableBody">
<!-- Move history entries will be added here dynamically -->
</tbody>
</table>
<button id="clearHistoryBtn" style="margin-top: 10px; padding: 5px 10px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Clear History</button>
</div>
</div>
</div>
</div>`;
contentContainer.innerHTML = content;
div.appendChild(contentContainer);
// Move history will be added later in the code
// Create keyboard shortcuts modal with improved styling
var keyboardModal = document.createElement('div');
keyboardModal.id = 'keyboardShortcutsModal';
keyboardModal.style = `
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 2000;
justify-content: center;
align-items: center;
`;
var modalContent = document.createElement('div');
modalContent.style = `
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
`;
var closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style = `
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #333;
transition: color 0.2s;
`;
closeBtn.onmouseover = function() {
this.style.color = '#F44336';
};
closeBtn.onmouseout = function() {
this.style.color = '#333';
};
closeBtn.onclick = function() {
keyboardModal.style.display = 'none';
};
modalContent.appendChild(closeBtn);
var shortcutsTitle = document.createElement('h2');
shortcutsTitle.textContent = 'Keyboard Shortcuts';
shortcutsTitle.style = 'margin-top: 0; color: #2196F3; border-bottom: 2px solid #eee; padding-bottom: 10px;';
modalContent.appendChild(shortcutsTitle);
// Add a brief description
var shortcutsDescription = document.createElement('p');
shortcutsDescription.textContent = 'Press any of these keys to quickly run the engine at different depths. Keys are organized by strength level.';
shortcutsDescription.style = 'margin-bottom: 20px; color: #666;';
modalContent.appendChild(shortcutsDescription);
// Add visual keyboard layout
var keyboardLayout = document.createElement('div');
keyboardLayout.style = `
background-color: #f5f5f5;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
text-align: center;
font-family: monospace;
`;
keyboardLayout.innerHTML = `
<div style="margin-bottom: 10px; font-weight: bold; color: #666;">Visual Keyboard Guide</div>
<div style="display: flex; justify-content: center; margin-bottom: 8px;">
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">Q<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #F44336;">1</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">W<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #F44336;">2</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">E<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #F44336;">3</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">R<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">4</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">T<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">5</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">Y<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">6</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">U<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">7</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">I<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">8</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">O<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #FF9800;">9</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">P<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">10</span></div>
</div>
<div style="display: flex; justify-content: center; margin-bottom: 8px; margin-left: 20px;">
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">A<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">11</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">S<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">12</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">D<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">13</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">F<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">14</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">G<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #4CAF50;">15</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">H<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #2196F3;">16</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">J<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #2196F3;">17</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">K<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #2196F3;">18</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">L<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #2196F3;">19</span></div>
</div>
<div style="display: flex; justify-content: center; margin-left: 40px;">
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">Z<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">20</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">X<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">21</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">C<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">22</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">V<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">23</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">B<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">24</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">N<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">25</span></div>
<div style="width: 40px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">M<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #9C27B0;">26</span></div>
</div>
<div style="margin-top: 15px; display: flex; justify-content: center;">
<div style="width: 80px; height: 40px; background-color: #e0e0e0; border: 1px solid #ccc; border-radius: 4px; display: flex; justify-content: center; align-items: center; margin: 0 2px; position: relative;">=<span style="position: absolute; bottom: 2px; right: 2px; font-size: 8px; color: #E91E63;">MAX</span></div>
</div>
<div style="margin-top: 15px; font-size: 12px;">
<span style="color: #F44336;">■</span> Beginner
<span style="color: #FF9800;">■</span> Intermediate
<span style="color: #4CAF50;">■</span> Advanced
<span style="color: #2196F3;">■</span> Expert
<span style="color: #9C27B0;">■</span> Master
<span style="color: #E91E63;">■</span> Maximum
</div>
`;
modalContent.appendChild(keyboardLayout);
var shortcutsTable = document.createElement('table');
shortcutsTable.style = 'width: 100%; border-collapse: collapse;';
// Create table header
var tableHeader = document.createElement('thead');
tableHeader.innerHTML = `
<tr style="background-color: #f5f5f5;">
<th style="text-align: left; padding: 12px; border-bottom: 2px solid #ddd; width: 20%;">Key</th>
<th style="text-align: left; padding: 12px; border-bottom: 2px solid #ddd;">Function</th>
<th style="text-align: left; padding: 12px; border-bottom: 2px solid #ddd;">Strength</th>
</tr>
`;
shortcutsTable.appendChild(tableHeader);
// Create table body with all keyboard shortcuts
var tableBody = document.createElement('tbody');
// Define all shortcuts with strength categories
const shortcuts = [
{ key: 'Q', function: 'Run engine at depth 1', strength: 'Beginner' },
{ key: 'W', function: 'Run engine at depth 2', strength: 'Beginner' },
{ key: 'E', function: 'Run engine at depth 3', strength: 'Beginner' },
{ key: 'R', function: 'Run engine at depth 4', strength: 'Intermediate' },
{ key: 'T', function: 'Run engine at depth 5', strength: 'Intermediate' },
{ key: 'Y', function: 'Run engine at depth 6', strength: 'Intermediate' },
{ key: 'U', function: 'Run engine at depth 7', strength: 'Intermediate' },
{ key: 'I', function: 'Run engine at depth 8', strength: 'Intermediate' },
{ key: 'O', function: 'Run engine at depth 9', strength: 'Intermediate' },
{ key: 'P', function: 'Run engine at depth 10', strength: 'Advanced' },
{ key: 'A', function: 'Run engine at depth 11', strength: 'Advanced' },
{ key: 'S', function: 'Run engine at depth 12', strength: 'Advanced' },
{ key: 'D', function: 'Run engine at depth 13', strength: 'Advanced' },
{ key: 'F', function: 'Run engine at depth 14', strength: 'Advanced' },
{ key: 'G', function: 'Run engine at depth 15', strength: 'Advanced' },
{ key: 'H', function: 'Run engine at depth 16', strength: 'Expert' },
{ key: 'J', function: 'Run engine at depth 17', strength: 'Expert' },
{ key: 'K', function: 'Run engine at depth 18', strength: 'Expert' },
{ key: 'L', function: 'Run engine at depth 19', strength: 'Expert' },
{ key: 'Z', function: 'Run engine at depth 20', strength: 'Master' },
{ key: 'X', function: 'Run engine at depth 21', strength: 'Master' },
{ key: 'C', function: 'Run engine at depth 22', strength: 'Master' },
{ key: 'V', function: 'Run engine at depth 23', strength: 'Master' },
{ key: 'B', function: 'Run engine at depth 24', strength: 'Master' },
{ key: 'N', function: 'Run engine at depth 25', strength: 'Master' },
{ key: 'M', function: 'Run engine at depth 26', strength: 'Master' },
{ key: '=', function: 'Run engine at maximum depth', strength: 'Maximum' }
];
// Add rows for each shortcut
shortcuts.forEach((shortcut, index) => {
const row = document.createElement('tr');
row.style = index % 2 === 0 ? '' : 'background-color: #f9f9f9;';
// Set color based on strength
let strengthColor = '#333';
switch(shortcut.strength) {
case 'Beginner': strengthColor = '#F44336'; break;
case 'Intermediate': strengthColor = '#FF9800'; break;
case 'Advanced': strengthColor = '#4CAF50'; break;
case 'Expert': strengthColor = '#2196F3'; break;
case 'Master': strengthColor = '#9C27B0'; break;
case 'Maximum': strengthColor = '#E91E63'; break;
}
row.innerHTML = `
<td style="padding: 10px; border-bottom: 1px solid #eee;">
<kbd style="background-color: #f1f1f1; border: 1px solid #ccc; border-radius: 4px; padding: 2px 6px; font-family: monospace;">${shortcut.key}</kbd>
</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">${shortcut.function}</td>
<td style="padding: 10px; border-bottom: 1px solid #eee; color: ${strengthColor};">${shortcut.strength}</td>
`;
tableBody.appendChild(row);
});
shortcutsTable.appendChild(tableBody);
modalContent.appendChild(shortcutsTable);
// Add a note at the bottom
var shortcutsNote = document.createElement('p');
shortcutsNote.innerHTML = '<strong>Note:</strong> Higher depths provide stronger analysis but take longer to calculate. For casual play, depths 1-10 are usually sufficient. For serious analysis, try depths 15+.';
shortcutsNote.style = 'margin-top: 20px; color: #666; font-size: 13px; background-color: #f5f5f5; padding: 10px; border-radius: 4px;';
modalContent.appendChild(shortcutsNote);
keyboardModal.appendChild(modalContent);
document.body.appendChild(keyboardModal);
// Function to check and adjust container position if it's too tall for the screen
function checkAndAdjustPosition() {
const container = document.getElementById('settingsContainer');
if (!container) return;
const rect = container.getBoundingClientRect();
const windowHeight = window.innerHeight;
// If the container extends beyond the bottom of the screen
if (rect.bottom > windowHeight) {
// Get current transform values
const transform = container.style.transform;
const match = transform.match(/translate3d\(([^,]+),\s*([^,]+),/);
if (match) {
const xPos = parseFloat(match[1]);
const yPos = parseFloat(match[2]);
// Calculate how much we need to move it up
const adjustment = Math.min(yPos, rect.bottom - windowHeight + 20);
if (adjustment > 0) {
const newYPos = yPos - adjustment;
container.style.transform = `translate3d(${xPos}px, ${newYPos}px, 0)`;
// Update the stored offset if available in the scope
if (typeof xOffset !== 'undefined' && typeof yOffset !== 'undefined') {
yOffset = newYPos;
// Save the adjusted position
try {
GM.setValue('GUI Position', { x: xOffset, y: yOffset });
} catch (error) {
console.error('Error saving adjusted position:', error);
}
}
}
}
}
}
// Add JavaScript for tab switching
setTimeout(function() {
const tabButtons = document.querySelectorAll('.tab-button');
const collapseBtn = document.getElementById('collapseBtn');
const aiControlsContent = document.getElementById('aiControlsContent');
const header = document.querySelector('#settingsContainer > div:first-child');
// Function to toggle content visibility
const toggleContent = () => {
if (aiControlsContent.style.display === 'none') {
aiControlsContent.style.display = 'block';
collapseBtn.style.transform = 'rotate(0deg)';
} else {
aiControlsContent.style.display = 'none';
collapseBtn.style.transform = 'rotate(180deg)';
}
};
// Add collapse functionality to button
if (collapseBtn && aiControlsContent) {
collapseBtn.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent header click event
toggleContent();
});
}
// Make header clickable
if (header && aiControlsContent) {
header.addEventListener('click', toggleContent);
}
// Handle Auto Move toggle
const autoMoveCheckbox = document.getElementById('autoMove');
const autoMoveStatus = document.getElementById('autoMoveStatus');
if (autoMoveCheckbox && autoMoveStatus) {
autoMoveCheckbox.addEventListener('change', function() {
autoMoveStatus.textContent = this.checked ? 'On' : 'Off';
autoMoveStatus.style.color = this.checked ? '#4CAF50' : '#666';
});
}
// Handle Auto Run toggle
const autoRunCheckbox = document.getElementById('autoRun');
const autoRunStatus = document.getElementById('autoRunStatus');
if (autoRunCheckbox && autoRunStatus) {
autoRunCheckbox.addEventListener('change', function() {
autoRunStatus.textContent = this.checked ? 'On' : 'Off';
autoRunStatus.style.color = this.checked ? '#FF9800' : '#666';
});
}
// Handle Auto Queue toggle
const autoQueueCheckbox = document.getElementById('autoQueue');
const autoQueueStatus = document.getElementById('autoQueueStatus');
if (autoQueueCheckbox && autoQueueStatus) {
autoQueueCheckbox.addEventListener('change', function() {
myVars.autoQueue = this.checked;
autoQueueStatus.textContent = this.checked ? 'On' : 'Off';
autoQueueStatus.style.color = this.checked ? '#9C27B0' : '#666';
// Update the observer based on the new setting
myFunctions.updateAutoQueueObserver();
});
}
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all tabs
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Add active class to clicked tab
this.classList.add('active');
document.getElementById(this.dataset.tab + '-tab').classList.add('active');
// Check and adjust position after tab switch (with a slight delay to allow rendering)
setTimeout(checkAndAdjustPosition, 100);
});
});
}, 500);
board.parentElement.parentElement.appendChild(div);
//spinnerContainer
var spinCont = document.createElement('div');
spinCont.setAttribute('style','display:none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; justify-content: center; align-items: center;');
spinCont.setAttribute('id','overlay');
div.prepend(spinCont);
//spinner
var spinr = document.createElement('div')
spinr.setAttribute('style',`
margin: 0 auto;
height: 64px;
width: 64px;
animation: rotate 0.8s infinite linear;
border: 5px solid firebrick;
border-right-color: transparent;
border-radius: 50%;
`);
spinCont.appendChild(spinr);
addAnimation(`@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}`);
//Reload Button
var reSty = `
#relButDiv {
position: relative;
text-align: center;
margin: 0 0 8px 0;
}
#relEngBut {
position: relative;
color: #ffffff;
background-color: #3cba2c;
font-size: 16px;
border: none;
border-radius: 4px;
padding: 10px 20px;
letter-spacing: 1px;
cursor: pointer;
transition: background-color 0.3s;
}
#relEngBut:hover {
background-color: #2d8c22;
}
#relEngBut:active {
background-color: #2d8c22;
transform: translateY(2px);
}`;
var reBut = `<button type="button" name="reloadEngine" id="relEngBut" onclick="document.myFunctions.reloadChessEngine()">Reload Chess Engine</button>`;
tmpDiv = document.createElement('div');
var relButDiv = document.createElement('div');
relButDiv.id = 'relButDiv';
tmpDiv.innerHTML = reBut;
reBut = tmpDiv.firstChild;
tmpStyle = document.createElement('style');
tmpStyle.innerHTML = reSty;
document.head.append(tmpStyle);
relButDiv.append(reBut);
contentContainer.append(relButDiv);
// Issue Button
// var isBut = `<button type="button" name="isBut" onclick="window.confirm('Can I take you to the issues page?') ? document.location = 'https://github.com/Auzgame/userscripts/issues' : console.log('cancled')">Got An Issue/Bug?</button>`;
// tmpDiv = document.createElement('div');
// var isButDiv = document.createElement('div');
// isButDiv.style = `
// position: relative;
// text-align: center;
// margin: 0 0 8px 0;
// `;
// tmpDiv.innerHTML = isBut;
// isBut = tmpDiv.firstChild;
// isBut.id = 'isBut';
// isBut.style = `
// position: relative;
// color: #ffffff;
// background-color: #919191;
// font-size: 16px;
// border: none;
// border-radius: 4px;
// padding: 10px 20px;
// letter-spacing: 1px;
// cursor: pointer;
// transition: background-color 0.3s;
// `;
// isButDiv.append(isBut);
// contentContainer.append(isButDiv);
// Add event listeners for the new buttons and controls
$('#applyDepth').on('click', function() {
myFunctions.runChessEngine();
});
$('#runEngineBtn').on('click', function() {
myFunctions.runChessEngine();
});
$('#stopEngineBtn').on('click', function() {
if (engine.engine) {
engine.engine.postMessage('stop');
isThinking = false;
myFunctions.spinner();
}
});
$('#saveSettingsBtn').on('click', function() {
myFunctions.saveSettings();
});
$('#showKeyboardShortcuts').on('click', function() {
document.getElementById('keyboardShortcutsModal').style.display = 'flex';
});
$('#clearHistoryBtn').on('click', function() {
document.getElementById('moveHistoryTableBody').innerHTML = '';
});
// Add collapse functionality
header.onclick = function() {
const content = document.getElementById('aiControlsContent');
const collapseBtn = document.getElementById('collapseBtn');
if (content.style.display === 'none') {
content.style.display = 'block';
collapseBtn.textContent = '▼';
// Check and adjust position after expanding
setTimeout(checkAndAdjustPosition, 100);
} else {
content.style.display = 'none';
collapseBtn.textContent = '▲';
}
};
// Add window resize event listener to adjust position when window is resized
window.addEventListener('resize', function() {
// Debounce the resize event to avoid excessive calculations
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout(function() {
checkAndAdjustPosition();
}, 200);
});
$('#evalBarColor').on('change', function() {
const theme = $(this).val();
if (theme === 'custom') {
$('#customColorContainer').show();
} else {
$('#customColorContainer').hide();
// Apply predefined color themes
let whiteColor, blackColor;
switch(theme) {
case 'blue':
whiteColor = '#2196F3'; // Blue
blackColor = '#FF9800'; // Orange
break;
case 'purple':
whiteColor = '#9C27B0'; // Purple
blackColor = '#FFEB3B'; // Yellow
break;
default: // default
whiteColor = '#4CAF50'; // Green
blackColor = '#F44336'; // Red
}
// Store colors in variables for the updateEvalBar function to use
myVars.whiteAdvantageColor = whiteColor;
myVars.blackAdvantageColor = blackColor;
// Update the evaluation bar with current value but new colors
updateEvalBar(myVars.currentEvaluation);
}
});
$('#whiteAdvantageColor, #blackAdvantageColor').on('change', function() {
myVars.whiteAdvantageColor = $('#whiteAdvantageColor').val();
myVars.blackAdvantageColor = $('#blackAdvantageColor').val();
updateEvalBar(myVars.currentEvaluation);
});
// Initialize color theme variables
myVars.whiteAdvantageColor = '#4CAF50';
myVars.blackAdvantageColor = '#F44336';
// Initialize fusion mode
myVars.fusionMode = false;
// Load saved settings
myFunctions.loadSettings();
// Update fusion mode status based on saved settings
if (myVars.fusionMode) {
myFunctions.updateFusionMode(true);
$('#fusionMode').prop('checked', true);
$('#eloSlider').prop('disabled', true);
}
// Check and adjust position after settings are loaded
setTimeout(checkAndAdjustPosition, 500);
// Periodically check for opponent rating changes when fusion mode is enabled
setInterval(function() {
if (myVars.fusionMode) {
extractOpponentRating();
}
}, 10000); // Check every 10 seconds
// Create ELO info modal
var eloInfoModal = document.createElement('div');
eloInfoModal.id = 'eloInfoModal';
eloInfoModal.style = `
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 2000;
justify-content: center;
align-items: center;
`;
var eloModalContent = document.createElement('div');
eloModalContent.style = `
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
position: relative;
`;
var eloCloseBtn = document.createElement('span');
eloCloseBtn.innerHTML = '×';
eloCloseBtn.style = `
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #333;
`;
eloCloseBtn.onclick = function() {
eloInfoModal.style.display = 'none';
};
eloModalContent.appendChild(eloCloseBtn);
var eloInfoTitle = document.createElement('h2');
eloInfoTitle.textContent = 'ELO Rating and Depth Relationship';
eloInfoTitle.style = 'margin-top: 0; color: #2196F3;';
eloModalContent.appendChild(eloInfoTitle);
var eloInfoText = document.createElement('div');
eloInfoText.innerHTML = `
<p>The ELO rating setting affects how strong the chess engine plays. Lower ELO ratings make the engine play more like a beginner, while higher ratings make it play more like a master.</p>
<p>To ensure the engine plays consistently with its ELO rating, the maximum search depth is limited based on the selected ELO:</p>
<ul>
<li><strong>1000-1199 ELO:</strong> Maximum depth 5 (Beginner level)</li>
<li><strong>1200-1499 ELO:</strong> Maximum depth 8 (Intermediate level)</li>
<li><strong>1500-1799 ELO:</strong> Maximum depth 12 (Advanced level)</li>
<li><strong>1800-2099 ELO:</strong> Maximum depth 15 (Expert level)</li>
<li><strong>2100-2399 ELO:</strong> Maximum depth 18 (Master level)</li>
<li><strong>2400+ ELO:</strong> Maximum depth 22 (Grandmaster level)</li>
</ul>
<p>If you set a depth higher than the maximum for the current ELO, it will be automatically limited to the maximum allowed depth.</p>
<p>This ensures that the engine plays consistently with its ELO rating and doesn't make moves that are too strong for the selected rating.</p>
`;
eloModalContent.appendChild(eloInfoText);
eloInfoModal.appendChild(eloModalContent);
document.body.appendChild(eloInfoModal);
$('#eloInfoBtn').on('click', function() {
document.getElementById('eloInfoModal').style.display = 'flex';
});
// Create Human Mode info modal
var humanModeInfoModal = document.createElement('div');
humanModeInfoModal.id = 'humanModeInfoModal';
humanModeInfoModal.style = `
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 2000;
justify-content: center;
align-items: center;
`;
var humanModeModalContent = document.createElement('div');
humanModeModalContent.style = `
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
position: relative;
`;
var humanModeCloseBtn = document.createElement('span');
humanModeCloseBtn.innerHTML = '×';
humanModeCloseBtn.style = `
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #333;
`;
humanModeCloseBtn.onclick = function() {
humanModeInfoModal.style.display = 'none';
};
humanModeModalContent.appendChild(humanModeCloseBtn);
var humanModeInfoTitle = document.createElement('h2');
humanModeInfoTitle.textContent = 'Human Mode: Realistic Chess Play';
humanModeInfoTitle.style = 'margin-top: 0; color: #2196F3;';
humanModeModalContent.appendChild(humanModeInfoTitle);
var humanModeInfoText = document.createElement('div');
humanModeInfoText.innerHTML = `
<p>Human Mode makes the chess engine play more like a real human player by introducing:</p>
<ul>
<li><strong>Realistic thinking time</strong> - varies based on skill level</li>
<li><strong>Occasional mistakes</strong> - humans don't always find the best move</li>
<li><strong>Rare blunders</strong> - even good players make serious mistakes sometimes</li>
</ul>
<p>Choose from five different skill levels:</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
<tr style="background-color: #f2f2f2;">
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Skill Level</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">ELO Range</th>
<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Characteristics</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Beginner</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">~800</td>
<td style="padding: 8px; border: 1px solid #ddd;">Quick moves, frequent mistakes, occasional blunders</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Casual</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">~1200</td>
<td style="padding: 8px; border: 1px solid #ddd;">Moderate thinking time, common mistakes</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Intermediate</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">~1600</td>
<td style="padding: 8px; border: 1px solid #ddd;">Longer thinking on complex positions, occasional mistakes</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Advanced</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">~2000</td>
<td style="padding: 8px; border: 1px solid #ddd;">Careful consideration, infrequent mistakes</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Expert</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">~2400</td>
<td style="padding: 8px; border: 1px solid #ddd;">Deep analysis, rare mistakes, very rare blunders</td>
</tr>
</table>
<p style="margin-top: 15px;"><strong>Note:</strong> Human Mode and Fusion Mode cannot be active at the same time.</p>
`;
humanModeModalContent.appendChild(humanModeInfoText);
humanModeInfoModal.appendChild(humanModeModalContent);
document.body.appendChild(humanModeInfoModal);
// Add CSS for toggle switch
var toggleStyle = document.createElement('style');
toggleStyle.innerHTML = `
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #4CAF50;
}
input:focus + .slider {
box-shadow: 0 0 1px #4CAF50;
}
input:checked + .slider:before {
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 24px;
}
.slider.round:before {
border-radius: 50%;
}
`;
document.head.appendChild(toggleStyle);
// Update auto run status when checkbox changes
$('#autoRun').on('change', function() {
const isChecked = this.checked;
myVars.autoRun = isChecked;
myFunctions.updateAutoRunStatus(isChecked ? 'on' : 'off');
});
$('#fusionMode').on('change', function() {
const isChecked = this.checked;
myFunctions.updateFusionMode(isChecked);
// Disable the ELO slider when fusion mode is enabled
$('#eloSlider').prop('disabled', isChecked);
// Extract opponent rating immediately when enabled
if (isChecked) {
extractOpponentRating();
}
// Disable human mode when fusion mode is enabled
if (isChecked && $('#humanMode').prop('checked')) {
$('#humanMode').prop('checked', false);
myFunctions.updateHumanMode(false);
}
});
// Human mode toggle event listener
$('#humanMode').on('change', function() {
const isChecked = this.checked;
myFunctions.updateHumanMode(isChecked);
// Disable the ELO slider when human mode is enabled
$('#eloSlider').prop('disabled', isChecked);
// Disable fusion mode when human mode is enabled
if (isChecked && $('#fusionMode').prop('checked')) {
$('#fusionMode').prop('checked', false);
myFunctions.updateFusionMode(false);
}
// Apply the selected human mode level
if (isChecked) {
const level = $('#humanModeSelect').val();
setHumanMode(level);
}
});
// Human mode level select event listener
$('#humanModeSelect').on('change', function() {
const level = $(this).val();
// Only apply if human mode is active
if ($('#humanMode').prop('checked')) {
setHumanMode(level);
}
});
// Human mode info button event listener
$('#humanModeInfoBtn').on('click', function() {
document.getElementById('humanModeInfoModal').style.display = 'flex';
});
$('#autoMove').on('change', function() {
myVars.autoMove = this.checked;
// Visual feedback for auto move toggle
if (this.checked) {
$(this).parent().append('<span id="autoMoveStatus" style="margin-left: 10px; font-size: 12px; color: #4CAF50;">On</span>');
} else {
$('#autoMoveStatus').remove();
}
});
$('#showArrows').on('change', function() {
myVars.showArrows = this.checked;
});
$('#persistentHighlights').on('change', function() {
myVars.persistentHighlights = this.checked;
// If turning off persistent highlights, clear any existing ones
if (!myVars.persistentHighlights) {
myFunctions.clearHighlights();
}
});
// Add event listener for the virtual chessboard toggle
$('#useVirtualChessboard').on('change', function() {
myVars.useVirtualChessboard = this.checked;
// Show/hide the virtual chessboard container based on the setting
const virtualChessboardContainer = document.getElementById('virtualChessboardContainer');
if (virtualChessboardContainer) {
virtualChessboardContainer.style.display = this.checked ? 'block' : 'none';
}
// If enabled, update the virtual chessboard with the current position
if (this.checked) {
myFunctions.updateVirtualChessboard();
// If we have a last move, show it on the virtual chessboard
if (myVars.lastMove) {
myFunctions.showVirtualMoveIndicator(myVars.lastMove.from, myVars.lastMove.to);
}
}
});
// Add event listeners for the move indicator type radio buttons
$('input[name="moveIndicatorType"]').on('change', function() {
myVars.moveIndicatorType = this.value;
// Update timestamp for settings synchronization
myVars.settings_last_updated = Date.now() / 1000;
// Clear any existing highlights and arrows when changing the indicator type
myFunctions.clearHighlights();
myFunctions.clearArrows();
// Show/hide arrow customization and animation containers based on selection
if (this.value === 'arrows') {
$('#arrowCustomizationContainer').slideDown(200);
$('#arrowAnimationContainer').slideDown(200);
} else {
$('#arrowCustomizationContainer').slideUp(200);
$('#arrowAnimationContainer').slideUp(200);
}
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
});
// Add event listeners for the arrow style radio buttons
$('input[name="arrowStyle"]').on('change', function() {
myVars.arrowStyle = this.value;
// Clear any existing arrows to apply the new style
myFunctions.clearArrows();
// If we're currently showing a move, redraw it with the new style
if (myVars.lastMove && myVars.moveIndicatorType === 'arrows') {
myFunctions.drawArrow(myVars.lastMove.from, myVars.lastMove.to, myVars.persistentHighlights);
}
});
// Add event listener for the arrow animation checkbox
$('#arrowAnimation').on('change', function() {
myVars.arrowAnimation = this.checked;
// Clear any existing arrows to apply the new animation setting
myFunctions.clearArrows();
// If we're currently showing a move, redraw it with the new animation setting
if (myVars.lastMove && myVars.moveIndicatorType === 'arrows') {
myFunctions.drawArrow(myVars.lastMove.from, myVars.lastMove.to, myVars.persistentHighlights);
}
});
// Add event listener for the multiple moves toggle
$('#showMultipleMoves').on('change', function() {
myVars.showMultipleMoves = this.checked;
// Update timestamp for settings synchronization
myVars.settings_last_updated = Date.now() / 1000;
// Update status text
$('#showMultipleMovesStatus').text(this.checked ? 'On' : 'Off');
$('#showMultipleMovesStatus').css('color', this.checked ? '#4CAF50' : '#666');
// Show/hide options
if (this.checked) {
$('#multipleMovesOptions').slideDown(200);
// If multicolor is enabled, hide arrow customization but keep animation container visible
if (myVars.useMulticolorMoves) {
$('#arrowCustomizationSection').slideUp(200);
$('#arrowAnimationContainer').slideDown(200);
}
} else {
$('#multipleMovesOptions').slideUp(200);
// Always show arrow customization when multiple moves is disabled
$('#arrowCustomizationSection').slideDown(200);
}
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
});
// Add event listener for the number of moves slider
$('#numberOfMovesToShow').on('input', function() {
const value = $(this).val();
$('#numberOfMovesValue').text(value);
myVars.numberOfMovesToShow = parseInt(value);
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
});
// Add event listener for the multicolor moves toggle
$('#useMulticolorMoves').on('change', function() {
myVars.useMulticolorMoves = this.checked;
// Update timestamp for settings synchronization
myVars.settings_last_updated = Date.now() / 1000;
// Update status text
$('#useMulticolorMovesStatus').text(this.checked ? 'On' : 'Off');
$('#useMulticolorMovesStatus').css('color', this.checked ? '#4CAF50' : '#666');
// Show/hide color pickers
if (this.checked) {
$('#moveColorOptions').slideDown(200);
$('#opacityNote').hide();
// Hide arrow customization section when multicolor is enabled
// but keep arrow animation container visible
$('#arrowCustomizationSection').slideUp(200);
} else {
$('#moveColorOptions').slideUp(200);
$('#opacityNote').show();
// Show arrow customization section when multicolor is disabled
$('#arrowCustomizationSection').slideDown(200);
}
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
});
// Add event listeners for the color pickers
for (let i = 1; i <= 5; i++) {
$(`#moveColor${i}`).on('change', function() {
myVars.moveColors[i] = $(this).val();
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
});
}
// Add event listener for the external window toggle
$('#useExternalWindow').on('change', function() {
myVars.useExternalWindow = this.checked;
// Show/hide external window options
if (this.checked) {
$('#externalWindowOptions').slideDown(200);
} else {
$('#externalWindowOptions').slideUp(200);
// Close the external window if it's open
if (myVars.externalWindowOpen && myVars.externalWindowRef) {
myVars.externalWindowRef.close();
myVars.externalWindowOpen = false;
myVars.externalWindowRef = null;
}
}
});
// Add event listener for the start server button
$('#startServerBtn').on('click', function() {
// Attempt to connect to the local server
myFunctions.checkServerConnection();
});
// Add event listener for the open external window button
$('#openExternalWindowBtn').on('click', function() {
myFunctions.openExternalWindow();
});
// Add event listener for the download server link
$('#downloadServerLink').on('click', function(e) {
e.preventDefault();
myFunctions.downloadServer();
});
// Add event listeners for move indicator location radio buttons
$('input[name="moveIndicatorLocation"]').on('change', function() {
myVars.moveIndicatorLocation = this.value;
console.log('Move indicator location set to:', this.value);
// Update timestamp for settings synchronization
myVars.settings_last_updated = Date.now() / 1000;
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
// If external window is open, update it
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
// Force an immediate update to the server
myFunctions.sendServerUpdate();
// If we're showing on the external board, run the engine to show the moves
if (this.value === 'external' || this.value === 'both') {
// Only run if we have a best move already
if (myVars.bestMove) {
console.log('Updating external board with current best move:', myVars.bestMove);
}
}
}
});
// Improved visual feedback for toggle switches
$('.switch input[type="checkbox"]').each(function() {
const statusElement = $('#' + this.id + 'Status');
if (statusElement.length) {
if (this.checked) {
statusElement.text('On');
statusElement.css('color', '#4CAF50');
} else {
statusElement.text('Off');
statusElement.css('color', '#666');
}
}
});
// Add visual feedback to buttons
$('#runEngineBtn, #stopEngineBtn, #saveSettingsBtn, #showKeyboardShortcuts, #applyDepth').each(function() {
$(this).css('transition', 'all 0.2s ease');
$(this).hover(
function() {
$(this).css({
'opacity': '0.9',
'transform': 'translateY(-1px)',
'box-shadow': '0 2px 5px rgba(0,0,0,0.2)'
});
},
function() {
$(this).css({
'opacity': '1',
'transform': 'translateY(0)',
'box-shadow': 'none'
});
}
);
$(this).mousedown(function() {
$(this).css('transform', 'translateY(1px)');
});
$(this).mouseup(function() {
$(this).css('transform', 'translateY(-1px)');
});
});
// Improve color theme selector
$('#evalBarColor').on('change', function() {
if (this.value === 'custom') {
$('#customColorContainer').slideDown(200);
} else {
$('#customColorContainer').slideUp(200);
}
});
// Add tooltips to buttons and controls
$('#runEngineBtn').attr('title', 'Analyze the current position with the chess engine');
$('#stopEngineBtn').attr('title', 'Stop the engine analysis');
$('#saveSettingsBtn').attr('title', 'Save your current settings for future sessions');
$('#depthSlider').attr('title', 'Higher depth = stronger analysis but slower calculation');
$('#showArrows').attr('title', 'Display arrows showing the best moves on the board');
$('#persistentHighlights').attr('title', 'Keep move highlights visible until the next move is made');
$('#autoRun').attr('title', 'Automatically run the engine after each move');
$('#autoMove').attr('title', 'Automatically make the best move for your side');
$('#timeDelayMin').attr('title', 'Minimum delay before auto-running the engine');
$('#timeDelayMax').attr('title', 'Maximum delay before auto-running the engine');
// Close modals when clicking outside
$('.modal-container').on('click', function(event) {
if (event.target === this) {
$(this).css('display', 'none');
}
});
// Add escape key to close modals
$(document).on('keydown', function(event) {
if (event.key === 'Escape') {
$('.modal-container').css('display', 'none');
}
});
// Add class to modals for easier selection
$('#keyboardShortcutsModal, #eloInfoModal, #humanModeInfoModal').addClass('modal-container');
loaded = true;
} catch (error) {console.log(error)}
}
function other(delay){
console.log(`Scheduling next auto run in ${delay/1000} seconds`);
myFunctions.updateAutoRunStatus('waiting');
// Use setTimeout instead of setInterval with constant checking
setTimeout(() => {
// Only proceed if auto run is still enabled
if(myVars.autoRun && myTurn && !isThinking) {
myFunctions.autoRun(lastValue);
}
canGo = true;
}, delay);
}
async function getVersion(){
try {
const response = await fetch('https://greasyfork.org/en/scripts/531171-chess-ai');
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const versionElement = doc.querySelector('dd.script-show-version span');
const version = versionElement.textContent;
console.log("Fetched version:", version);
console.log("Current version:", currentVersion);
if(currentVersion !== version){
console.log("Version mismatch detected!");
if (document.hasFocus()) {
alert('UPDATE THIS SCRIPT IN ORDER TO PROCEED!');
window.open('https://greasyfork.org/en/scripts/531171-chess-ai', '_blank');
}
// Recursive call to keep displaying the popup
setTimeout(getVersion, 1000); // Call again after 1 second
} else {
console.log("Version check passed");
}
} catch (error) {
console.error("Error fetching version:", error);
// Recursive call to keep trying to fetch the version
setTimeout(getVersion, 1000); // Call again after 1 second
}
}
getVersion();
const waitForChessBoard = setInterval(() => {
if(loaded) {
board = $('chess-board')[0] || $('wc-chess-board')[0];
// Only update these values when needed, not every 100ms
if($('#autoRun')[0]) myVars.autoRun = $('#autoRun')[0].checked;
if($('#autoMove')[0]) myVars.autoMove = $('#autoMove')[0].checked;
if($('#showArrows')[0]) myVars.showArrows = $('#showArrows')[0].checked;
// Update move indicator type if radio buttons exist
if($('input[name="moveIndicatorType"]:checked')[0]) {
myVars.moveIndicatorType = $('input[name="moveIndicatorType"]:checked')[0].value;
}
// Check if turn has changed
const currentTurn = board.game.getTurn() == board.game.getPlayingAs();
const turnChanged = currentTurn !== myTurn;
myTurn = currentTurn;
// Only update delay values when needed
if($('#timeDelayMin')[0] && $('#timeDelayMax')[0]) {
let minDel = parseFloat($('#timeDelayMin')[0].value);
let maxDel = parseFloat($('#timeDelayMax')[0].value);
myVars.delay = Math.random() * (maxDel - minDel) + minDel;
}
myVars.isThinking = isThinking;
myFunctions.spinner();
// If turn has changed to player's turn and auto run is enabled, trigger auto run
if(turnChanged && myTurn && myVars.autoRun && canGo && !isThinking) {
console.log("Turn changed to player's turn, triggering auto run");
canGo = false;
var currentDelay = myVars.delay != undefined ? myVars.delay * 1000 : 10;
other(currentDelay);
}
// Update evaluation bar position if board size changes
if(evalBar && evalText && board) {
evalText.style.left = `${evalBar.offsetLeft}px`;
}
} else {
myFunctions.loadEx();
}
if(!engine.engine){
myFunctions.loadChessEngine();
}
// Check if the board exists and we haven't set up the move listener yet
if (board && !board._highlightListenerAdded) {
// Try to add a listener for moves
try {
// Store the current position FEN to detect changes
myVars.lastPositionFEN = board.game.getFEN();
// Mark that we've added the listener
board._highlightListenerAdded = true;
} catch (err) {
console.log('Error setting up move listener:', err);
}
}
// Check if the position has changed (a move was made)
if (board && myVars.lastPositionFEN) {
const currentFEN = board.game.getFEN();
if (currentFEN !== myVars.lastPositionFEN) {
// Position changed, clear highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
myVars.lastPositionFEN = currentFEN;
// No need to check for game end here anymore as we're using MutationObserver
}
}
}, 100);
// Function to check server connection
myFunctions.checkServerConnection = function() {
// Update server status
$('#serverStatusText').text('Checking...');
// Try to connect to the local server
fetch('http://localhost:8765/api/status')
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Server not responding');
})
.then(data => {
if (data.status === 'running') {
$('#serverStatusText').text('Running');
$('#serverStatusText').css('color', '#4CAF50');
myVars.serverConnected = true;
// Enable the open window button
$('#openExternalWindowBtn').prop('disabled', false);
// Apply main controls visibility based on current settings
myFunctions.updateMainControlsVisibility();
// Start sending updates to the server if the external window is open
if (myVars.externalWindowOpen) {
myFunctions.startServerUpdates();
}
} else {
$('#serverStatusText').text('Error: ' + data.status);
$('#serverStatusText').css('color', '#F44336');
myVars.serverConnected = false;
}
})
.catch(error => {
console.error('Server connection error:', error);
$('#serverStatusText').text('Not Running - Start Python Server');
$('#serverStatusText').css('color', '#F44336');
myVars.serverConnected = false;
// Show instructions for starting the server
const notification = document.createElement('div');
notification.innerHTML = `
<p>To use the external window feature, you need to run the Python server:</p>
<ol>
<li><a href="#" id="downloadServerLink2" style="color: #2196F3; text-decoration: underline;">Download the chess_ai_server.py file</a> to your computer</li>
<li>Open a command prompt or terminal</li>
<li>Navigate to the folder containing the file</li>
<li>Run: <code>python chess_ai_server.py</code></li>
</ol>
<p>Then click "Check Server Connection" again.</p>
`;
notification.style = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 10000;
max-width: 400px;
font-family: "Segoe UI", Arial, sans-serif;
`;
// Add close button
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.style = `
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
`;
closeBtn.onclick = function() {
document.body.removeChild(notification);
};
notification.appendChild(closeBtn);
document.body.appendChild(notification);
// Add event listener for the second download server link
$('#downloadServerLink2').on('click', function(e) {
e.preventDefault();
myFunctions.downloadServer();
// Close the notification after starting the download
document.body.removeChild(notification);
});
});
};
// Function to update the visibility of main controls based on settings
myFunctions.updateMainControlsVisibility = function() {
// Only apply this when connected to the external window
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
const settingsContainer = $('#settingsContainer');
if (myVars.disableMainControls) {
console.log('Disabling main controls as requested by external window');
// Hide the main controls
if (settingsContainer.length) {
settingsContainer.css('display', 'none');
}
// Create or update a notification to inform the user
let notification = $('#externalControlsNotification');
if (notification.length === 0) {
notification = $('<div id="externalControlsNotification" style="position: fixed; top: 10px; right: 10px; background-color: rgba(33, 150, 243, 0.9); color: white; padding: 10px; border-radius: 5px; z-index: 9999; font-family: Arial, sans-serif; box-shadow: 0 2px 10px rgba(0,0,0,0.2); max-width: 300px;">' +
'<div style="font-weight: bold; margin-bottom: 5px;">Chess AI Controls Disabled</div>' +
'<div style="font-size: 12px;">Main controls are hidden while using the external window. Disable this option in the external window interface settings to show controls here again.</div>' +
'</div>');
$('body').append(notification);
} else {
notification.show();
}
} else {
console.log('Enabling main controls as requested by external window');
// Show the main controls
if (settingsContainer.length) {
settingsContainer.css('display', 'block');
}
// Hide the notification if it exists
$('#externalControlsNotification').hide();
}
} else {
// Always show controls when not connected to external window
const settingsContainer = $('#settingsContainer');
if (settingsContainer.length) {
settingsContainer.css('display', 'block');
}
// Hide the notification if it exists
$('#externalControlsNotification').hide();
}
};
// Function to send updates to the server
myFunctions.sendServerUpdate = function() {
if (!myVars.serverConnected || !myVars.externalWindowOpen) {
return;
}
// Get the current board position
const fen = myVars.chess ? myVars.chess.fen() : '';
// Debug log for top moves
console.log('Sending top moves to server:', myVars.topMoves);
if (myVars.topMoves && myVars.topMoves.length > 0) {
console.log('First move:', myVars.topMoves[0]);
if (myVars.topMoves.length > 1) {
console.log('Second move:', myVars.topMoves[1]);
}
}
// Add timestamp for settings synchronization
if (!myVars.settings_last_updated) {
myVars.settings_last_updated = Date.now() / 1000; // Convert to seconds to match Python's time.time()
}
// Prepare the data to send with all visual settings
const data = {
fen: fen,
evaluation: myVars.currentEvaluation,
best_move: myVars.bestMove || '',
engine_running: myVars.engineRunning || false,
top_moves: myVars.topMoves || [],
depth: parseInt($('#depthSlider')[0].value) || 11,
elo: myVars.eloRating || 1500,
// Settings synchronization metadata
settings_last_updated: myVars.settings_last_updated,
settings_update_source: 'userscript',
// Automation settings
auto_move: myVars.autoMove || false,
auto_run: myVars.autoRun || false,
auto_run_delay_min: myVars.delayMin || myVars.delay || 0.1,
auto_run_delay_max: myVars.delayMax || myVars.delay || 1.0,
// Interface settings
disable_main_controls: myVars.disableMainControls || false,
// Move indicator settings
move_indicator_location: myVars.moveIndicatorLocation || 'main',
move_indicator_type: myVars.moveIndicatorType || 'highlights',
persistent_highlights: myVars.persistentHighlights !== undefined ? myVars.persistentHighlights : true,
// Multiple moves settings
show_multiple_moves: myVars.showMultipleMoves !== undefined ? myVars.showMultipleMoves : false,
number_of_moves_to_show: myVars.numberOfMovesToShow || 3,
use_multicolor_moves: myVars.useMulticolorMoves !== undefined ? myVars.useMulticolorMoves : false,
move_colors: myVars.moveColors || {},
// Arrow settings
arrow_style: myVars.arrowStyle || 'curved',
arrow_animation: myVars.arrowAnimation !== undefined ? myVars.arrowAnimation : true,
arrow_color: myVars.arrowColor || '#0077CC',
arrow_opacity: 0.8,
// Evaluation bar colors
white_advantage_color: myVars.whiteAdvantageColor || '#4CAF50',
black_advantage_color: myVars.blackAdvantageColor || '#F44336'
};
// Send the data to the server
fetch('http://localhost:8765/api/update_state', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to update server state');
}
return response.json();
})
.then(data => {
// Server update successful
console.log('Server update successful:', data);
})
.catch(error => {
console.error('Error updating server:', error);
// If we can't connect to the server, mark as disconnected
if (myVars.serverConnected) {
myVars.serverConnected = false;
$('#serverStatusText').text('Connection Lost');
$('#serverStatusText').css('color', '#F44336');
// Stop the update interval
myFunctions.stopServerUpdates();
}
});
};
// Function to check for pending commands from the server
myFunctions.checkPendingCommands = function() {
if (!myVars.serverConnected || !myVars.externalWindowOpen) {
return;
}
// Fetch pending commands from the server
fetch('http://localhost:8765/api/pending_commands')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch pending commands');
}
return response.json();
})
.then(data => {
// Process any pending commands
if (data.commands && data.commands.length > 0) {
console.log('Received pending commands from server:', data.commands);
// Process each command in order
data.commands.forEach(commandData => {
myFunctions.handleServerCommand(commandData.command, commandData.params);
});
}
})
.catch(error => {
console.error('Error checking pending commands:', error);
// If we can't connect to the server, mark as disconnected
if (myVars.serverConnected) {
myVars.serverConnected = false;
$('#serverStatusText').text('Connection Lost');
$('#serverStatusText').css('color', '#F44336');
// Stop the update intervals
myFunctions.stopServerUpdates();
}
});
};
// Function to start sending updates to the server
myFunctions.startServerUpdates = function() {
// Stop any existing interval
myFunctions.stopServerUpdates();
// Start a new interval for sending updates
myVars.serverUpdateInterval = setInterval(myFunctions.sendServerUpdate, 1000);
// Start a new interval for checking pending commands
myVars.commandCheckInterval = setInterval(myFunctions.checkPendingCommands, 1000);
// Send an initial update
myFunctions.sendServerUpdate();
// Check for pending commands immediately
myFunctions.checkPendingCommands();
};
// Function to stop sending updates to the server
myFunctions.stopServerUpdates = function() {
if (myVars.serverUpdateInterval) {
clearInterval(myVars.serverUpdateInterval);
myVars.serverUpdateInterval = null;
}
if (myVars.commandCheckInterval) {
clearInterval(myVars.commandCheckInterval);
myVars.commandCheckInterval = null;
}
};
// Function to run the engine (wrapper for runChessEngine)
myFunctions.runEngine = function() {
myFunctions.runChessEngine();
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen) {
myFunctions.sendServerUpdate();
}
};
// Function to stop the engine
myFunctions.stopEngine = function() {
if (engine.engine) {
engine.engine.postMessage('stop');
isThinking = false;
myFunctions.spinner();
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen) {
myFunctions.sendServerUpdate();
}
}
};
// Function to handle commands from the server
myFunctions.handleServerCommand = function(command, params) {
console.log('Received command from server:', command, params);
switch (command) {
case 'run_engine':
// Run the engine with the specified depth
if (params && params.depth) {
$('#depthSlider')[0].value = params.depth;
$('#depthValue').text(params.depth);
}
myFunctions.runEngine();
break;
case 'stop_engine':
// Stop the engine
myFunctions.stopEngine();
break;
case 'toggle_auto_move':
// Toggle auto move or set to specific state
if (params && params.state !== undefined) {
// Set to specific state
if (myVars.autoMove !== params.state) {
$('#autoMove')[0].click();
}
} else {
// Just toggle
$('#autoMove')[0].click();
}
break;
case 'toggle_auto_run':
// Toggle auto run or set to specific state
if (params && params.state !== undefined) {
// Set to specific state
if (myVars.autoRun !== params.state) {
$('#autoRun')[0].click();
}
} else {
// Just toggle
$('#autoRun')[0].click();
}
break;
case 'update_auto_run_delay':
// Update the auto run delay
if (params) {
const minDelay = params.min_delay !== undefined ? parseFloat(params.min_delay) : undefined;
const maxDelay = params.max_delay !== undefined ? parseFloat(params.max_delay) : undefined;
console.log('Updating auto run delay to', minDelay, '-', maxDelay);
// Update the delay in myVars
if (minDelay !== undefined) {
// If we have both min and max, set a random value in between
if (maxDelay !== undefined) {
// Store both values for future reference
myVars.delayMin = minDelay;
myVars.delayMax = maxDelay;
// Set the current delay to a random value in the range
myVars.delay = Math.random() * (maxDelay - minDelay) + minDelay;
} else {
// If we only have min, use it for both
myVars.delay = minDelay;
myVars.delayMin = minDelay;
myVars.delayMax = minDelay;
}
} else if (maxDelay !== undefined) {
// If we only have max, use it for both
myVars.delay = maxDelay;
myVars.delayMin = maxDelay;
myVars.delayMax = maxDelay;
}
// Update the UI if the delay inputs exist
if ($('#timeDelayMin')[0] && $('#timeDelayMax')[0]) {
if (minDelay !== undefined) $('#timeDelayMin')[0].value = minDelay;
if (maxDelay !== undefined) $('#timeDelayMax')[0].value = maxDelay;
}
}
break;
case 'update_depth':
// Update the depth
if (params && params.depth) {
$('#depthSlider')[0].value = params.depth;
$('#depthValue').text(params.depth);
}
break;
case 'update_elo':
// Update the ELO rating
if (params && params.elo) {
myVars.eloRating = params.elo;
$('#eloValue').text(params.elo);
}
break;
case 'update_visual_settings':
// Update visual settings from the external window
if (params) {
console.log('Updating visual settings from server:', params);
// Check if we should update based on timestamp
const serverTimestamp = params.settings_last_updated || 0;
const currentTimestamp = myVars.settings_last_updated || 0;
console.log(`Received settings update - Current timestamp: ${currentTimestamp}, Server timestamp: ${serverTimestamp}`);
// Only update if the server settings are newer than our current settings
if (serverTimestamp > currentTimestamp) {
console.log('Applying newer settings from external board');
// Update our timestamp to match the server's
myVars.settings_last_updated = serverTimestamp;
// Update move indicator location
if (params.move_indicator_location) {
myVars.moveIndicatorLocation = params.move_indicator_location;
$('input[name="moveIndicatorLocation"][value="' + params.move_indicator_location + '"]').prop('checked', true);
}
// Update move indicator type
if (params.move_indicator_type) {
myVars.moveIndicatorType = params.move_indicator_type;
$('input[name="moveIndicatorType"][value="' + params.move_indicator_type + '"]').prop('checked', true);
// Show/hide arrow options based on selection
if (params.move_indicator_type === 'arrows') {
$('#arrowOptions').show();
} else {
$('#arrowOptions').hide();
}
}
// Update multiple moves settings
if (params.show_multiple_moves !== undefined) {
myVars.showMultipleMoves = params.show_multiple_moves;
$('#showMultipleMoves').prop('checked', params.show_multiple_moves);
// Show/hide multiple moves options
if (params.show_multiple_moves) {
$('#multipleMovesOptions').show();
} else {
$('#multipleMovesOptions').hide();
}
}
// Update number of moves to show
if (params.number_of_moves_to_show) {
myVars.numberOfMovesToShow = params.number_of_moves_to_show;
$('#numberOfMovesToShow').val(params.number_of_moves_to_show);
}
// Update multicolor moves setting
if (params.use_multicolor_moves !== undefined) {
myVars.useMulticolorMoves = params.use_multicolor_moves;
$('#useMulticolorMoves').prop('checked', params.use_multicolor_moves);
// Show/hide color options
if (params.use_multicolor_moves) {
$('#moveColorsContainer').show();
$('#opacityNote').hide();
} else {
$('#moveColorsContainer').hide();
$('#opacityNote').show();
}
}
// Update arrow style
if (params.arrow_style) {
myVars.arrowStyle = params.arrow_style;
$('input[name="arrowStyle"][value="' + params.arrow_style + '"]').prop('checked', true);
}
// Update arrow animation
if (params.arrow_animation !== undefined) {
myVars.arrowAnimation = params.arrow_animation;
$('#arrowAnimation').prop('checked', params.arrow_animation);
}
// Update evaluation bar colors
if (params.white_advantage_color) {
myVars.whiteAdvantageColor = params.white_advantage_color;
$('#whiteAdvantageColor').val(params.white_advantage_color);
}
if (params.black_advantage_color) {
myVars.blackAdvantageColor = params.black_advantage_color;
$('#blackAdvantageColor').val(params.black_advantage_color);
}
// Clear any existing highlights and arrows
myFunctions.clearHighlights();
myFunctions.clearArrows();
// Save the settings
myFunctions.saveSettings();
// If the engine is running, update the display
if (myVars.engineRunning) {
myFunctions.runEngine();
}
// Send an acknowledgment update back to the server with the new timestamp
// This prevents update loops by confirming we've received and applied the settings
setTimeout(() => {
myFunctions.sendServerUpdate();
}, 500);
} else {
console.log('Ignoring older settings from external board');
}
}
break;
case 'update_interface_settings':
// Update interface settings from the external window
if (params) {
console.log('Updating interface settings from server:', params);
// Check if we should update based on timestamp
const serverTimestamp = params.settings_last_updated || 0;
const currentTimestamp = myVars.settings_last_updated || 0;
console.log(`Received interface settings update - Current timestamp: ${currentTimestamp}, Server timestamp: ${serverTimestamp}`);
// Only update if the server settings are newer than our current settings
if (serverTimestamp > currentTimestamp) {
console.log('Applying newer interface settings from external board');
// Update our timestamp to match the server's
myVars.settings_last_updated = serverTimestamp;
// Update disable main controls setting
if (params.disable_main_controls !== undefined) {
myVars.disableMainControls = params.disable_main_controls;
// Apply the setting immediately
myFunctions.updateMainControlsVisibility();
}
// Save the settings
myFunctions.saveSettings();
// Send an acknowledgment update back to the server with the new timestamp
// This prevents update loops by confirming we've received and applied the settings
setTimeout(() => {
myFunctions.sendServerUpdate();
}, 500);
} else {
console.log('Ignoring older interface settings from external board');
}
}
break;
default:
console.warn('Unknown command from server:', command);
}
};
// Function to open the external window
myFunctions.openExternalWindow = function() {
// Check if server is connected
if (!myVars.serverConnected) {
myFunctions.checkServerConnection();
return;
}
// Check if window is already open
if (myVars.externalWindowOpen && myVars.externalWindowRef && !myVars.externalWindowRef.closed) {
// Focus the existing window
myVars.externalWindowRef.focus();
return;
}
// Open a new window
myVars.externalWindowRef = window.open('http://localhost:8765', 'ChessAIControls',
'width=800,height=600,resizable=yes,scrollbars=yes,status=yes');
if (myVars.externalWindowRef) {
myVars.externalWindowOpen = true;
// Set up event listener for when the window is closed
myVars.externalWindowRef.addEventListener('beforeunload', function() {
myVars.externalWindowOpen = false;
myVars.externalWindowRef = null;
// Stop sending updates to the server
myFunctions.stopServerUpdates();
// Always show main controls when external window is closed
myVars.disableMainControls = false;
myFunctions.updateMainControlsVisibility();
});
// Apply main controls visibility based on current settings
myFunctions.updateMainControlsVisibility();
// Start sending updates to the server
myFunctions.startServerUpdates();
} else {
// Window was blocked by popup blocker
alert('The external window was blocked by your browser. Please allow popups for this site.');
}
};
// Function to save user settings using GM.setValue asynchronously
myFunctions.saveSettings = async function() {
// Update timestamp for settings synchronization
myVars.settings_last_updated = Date.now() / 1000;
const settings = {
eloRating: myVars.eloRating,
depth: parseInt($('#depthSlider')[0].value),
showArrows: $('#showArrows')[0].checked,
persistentHighlights: $('#persistentHighlights')[0].checked,
moveIndicatorType: myVars.moveIndicatorType || 'highlights',
autoRun: $('#autoRun')[0].checked,
autoMove: $('#autoMove')[0].checked,
autoQueue: $('#autoQueue')[0].checked,
timeDelayMin: parseFloat($('#timeDelayMin')[0].value),
timeDelayMax: parseFloat($('#timeDelayMax')[0].value),
evalBarTheme: $('#evalBarColor').val(),
whiteAdvantageColor: $('#whiteAdvantageColor').val(),
blackAdvantageColor: $('#blackAdvantageColor').val(),
arrowColor: $('#arrowColor').val(),
settings_last_updated: myVars.settings_last_updated,
arrowStyle: $('input[name="arrowStyle"]:checked').val() || 'curved',
arrowAnimation: $('#arrowAnimation')[0].checked,
showMultipleMoves: $('#showMultipleMoves')[0].checked,
numberOfMovesToShow: parseInt($('#numberOfMovesToShow')[0].value),
useMulticolorMoves: $('#useMulticolorMoves')[0].checked,
moveColors: {
1: $('#moveColor1').val() || '#F44336',
2: $('#moveColor2').val() || '#FF9800',
3: $('#moveColor3').val() || '#FFEB3B',
4: $('#moveColor4').val() || '#4CAF50',
5: $('#moveColor5').val() || '#2196F3'
},
useVirtualChessboard: $('#useVirtualChessboard')[0].checked,
useExternalWindow: $('#useExternalWindow')[0].checked,
disableMainControls: myVars.disableMainControls || false,
fusionMode: myVars.fusionMode,
humanMode: myVars.humanMode ? {
active: myVars.humanMode.active,
level: myVars.humanMode.level
} : { active: false, level: 'intermediate' }
};
try {
await GM.setValue('chessAISettings', JSON.stringify(settings));
// Show saved notification (same as before)
const notification = document.createElement('div');
notification.textContent = 'Settings saved!';
notification.style = `
position: fixed;
bottom: 20px;
right: 20px;
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
}, 10);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 2000);
// Update the server if external window is open
if (myVars.useExternalWindow && myVars.externalWindowOpen && myVars.serverConnected) {
myFunctions.sendServerUpdate();
}
} catch (error) {
console.error('Error saving settings:', error);
// Handle error as needed, e.g., show an error notification
}
};
// Function to load user settings using await GM.getValue
myFunctions.loadSettings = async function() {
try {
// First try to load settings from the combined JSON
const savedSettings = await GM.getValue('chessAISettings', null);
if (savedSettings) {
// If settings exist as JSON, parse and apply them
const settings = JSON.parse(savedSettings);
// Apply saved settings to myVars
myVars.eloRating = settings.eloRating || 1500;
myVars.depth = settings.depth || 11;
myVars.showArrows = settings.showArrows !== undefined ? settings.showArrows : true;
myVars.persistentHighlights = settings.persistentHighlights !== undefined ? settings.persistentHighlights : true;
myVars.moveIndicatorType = settings.moveIndicatorType || 'highlights';
myVars.autoRun = settings.autoRun !== undefined ? settings.autoRun : false;
myVars.autoMove = settings.autoMove !== undefined ? settings.autoMove : false;
myVars.autoQueue = settings.autoQueue !== undefined ? settings.autoQueue : false;
myVars.fusionMode = settings.fusionMode !== undefined ? settings.fusionMode : false;
myVars.whiteAdvantageColor = settings.whiteAdvantageColor || '#4CAF50';
myVars.blackAdvantageColor = settings.blackAdvantageColor || '#F44336';
myVars.arrowColor = settings.arrowColor || '#0077CC';
myVars.arrowStyle = settings.arrowStyle || 'curved';
myVars.arrowAnimation = settings.arrowAnimation !== undefined ? settings.arrowAnimation : true;
myVars.showMultipleMoves = settings.showMultipleMoves !== undefined ? settings.showMultipleMoves : false;
myVars.numberOfMovesToShow = settings.numberOfMovesToShow || 3;
myVars.useMulticolorMoves = settings.useMulticolorMoves !== undefined ? settings.useMulticolorMoves : false;
myVars.useVirtualChessboard = settings.useVirtualChessboard !== undefined ? settings.useVirtualChessboard : false;
myVars.useExternalWindow = settings.useExternalWindow !== undefined ? settings.useExternalWindow : false;
myVars.disableMainControls = settings.disableMainControls !== undefined ? settings.disableMainControls : false;
// Load settings timestamp if available, or initialize it
myVars.settings_last_updated = settings.settings_last_updated || (Date.now() / 1000);
// Load move colors if they exist
if (settings.moveColors) {
myVars.moveColors = {
1: settings.moveColors[1] || '#F44336',
2: settings.moveColors[2] || '#FF9800',
3: settings.moveColors[3] || '#FFEB3B',
4: settings.moveColors[4] || '#4CAF50',
5: settings.moveColors[5] || '#2196F3'
};
}
// Set humanMode
if (settings.humanMode) {
myVars.humanMode = {
active: settings.humanMode.active,
level: settings.humanMode.level
};
} else {
myVars.humanMode = { active: false, level: 'intermediate' };
}
// Update UI elements
if ($('#depthSlider')[0]) {
$('#depthSlider')[0].value = myVars.depth;
$('#depthText').html('Current Depth: <strong>' + myVars.depth + '</strong>');
}
if ($('#eloSlider')[0]) {
$('#eloSlider')[0].value = myVars.eloRating;
$('#eloText').html('Current ELO: <strong>' + myVars.eloRating + '</strong>');
}
if ($('#autoMove')[0]) {
$('#autoMove')[0].checked = myVars.autoMove;
}
if ($('#autoRun')[0]) {
$('#autoRun')[0].checked = myVars.autoRun;
}
if ($('#autoQueue')[0]) {
$('#autoQueue')[0].checked = myVars.autoQueue;
$('#autoQueueStatus').text(myVars.autoQueue ? 'On' : 'Off');
$('#autoQueueStatus').css('color', myVars.autoQueue ? '#9C27B0' : '#666');
}
if ($('#showArrows')[0]) {
$('#showArrows')[0].checked = myVars.showArrows;
}
if ($('#persistentHighlights')[0]) {
$('#persistentHighlights')[0].checked = myVars.persistentHighlights;
}
if ($('input[name="moveIndicatorType"]').length) {
$('input[name="moveIndicatorType"][value="' + myVars.moveIndicatorType + '"]').prop('checked', true);
// Show/hide arrow customization and animation containers based on move indicator type
if (myVars.moveIndicatorType === 'arrows') {
$('#arrowCustomizationContainer').show();
$('#arrowAnimationContainer').show();
} else {
$('#arrowCustomizationContainer').hide();
$('#arrowAnimationContainer').hide();
}
}
if ($('#humanMode')[0] && myVars.humanMode) {
$('#humanMode')[0].checked = myVars.humanMode.active;
}
if ($('#humanLevelSelect')[0] && myVars.humanMode) {
$('#humanLevelSelect')[0].value = myVars.humanMode.level;
}
if ($('#fusionMode')[0]) {
$('#fusionMode')[0].checked = myVars.fusionMode;
}
if ($('#whiteAdvantageColor')[0]) {
$('#whiteAdvantageColor')[0].value = myVars.whiteAdvantageColor;
}
if ($('#blackAdvantageColor')[0]) {
$('#blackAdvantageColor')[0].value = myVars.blackAdvantageColor;
}
if ($('#arrowColor')[0]) {
$('#arrowColor')[0].value = myVars.arrowColor;
}
// Set arrow style radio button
if (myVars.arrowStyle) {
if (myVars.arrowStyle === 'curved' && $('#arrowStyleCurved')[0]) {
$('#arrowStyleCurved')[0].checked = true;
} else if (myVars.arrowStyle === 'straight' && $('#arrowStyleStraight')[0]) {
$('#arrowStyleStraight')[0].checked = true;
}
}
// Set arrow animation checkbox
if ($('#arrowAnimation')[0]) {
$('#arrowAnimation')[0].checked = myVars.arrowAnimation !== undefined ? myVars.arrowAnimation : true;
}
// Set multiple moves toggle
if ($('#showMultipleMoves')[0]) {
$('#showMultipleMoves')[0].checked = myVars.showMultipleMoves;
$('#showMultipleMovesStatus').text(myVars.showMultipleMoves ? 'On' : 'Off');
$('#showMultipleMovesStatus').css('color', myVars.showMultipleMoves ? '#4CAF50' : '#666');
if (myVars.showMultipleMoves) {
$('#multipleMovesOptions').show();
} else {
$('#multipleMovesOptions').hide();
}
}
// Set number of moves slider
if ($('#numberOfMovesToShow')[0]) {
$('#numberOfMovesToShow')[0].value = myVars.numberOfMovesToShow;
$('#numberOfMovesValue').text(myVars.numberOfMovesToShow);
}
// Set multicolor moves toggle
if ($('#useMulticolorMoves')[0]) {
$('#useMulticolorMoves')[0].checked = myVars.useMulticolorMoves;
$('#useMulticolorMovesStatus').text(myVars.useMulticolorMoves ? 'On' : 'Off');
$('#useMulticolorMovesStatus').css('color', myVars.useMulticolorMoves ? '#4CAF50' : '#666');
if (myVars.useMulticolorMoves) {
$('#moveColorOptions').show();
$('#opacityNote').hide();
// Hide arrow customization section when multicolor is enabled
// but keep arrow animation container visible
$('#arrowCustomizationSection').hide();
} else {
$('#moveColorOptions').hide();
$('#opacityNote').show();
// Show arrow customization section when multicolor is disabled
$('#arrowCustomizationSection').show();
}
}
// Always show arrow animation container if arrows are enabled
if (myVars.moveIndicatorType === 'arrows' && $('#arrowAnimationContainer')[0]) {
$('#arrowAnimationContainer').show();
}
// Set color pickers
if (myVars.moveColors) {
for (let i = 1; i <= 5; i++) {
if ($(`#moveColor${i}`)[0] && myVars.moveColors[i]) {
$(`#moveColor${i}`)[0].value = myVars.moveColors[i];
}
}
}
// Set virtual chessboard toggle
if ($('#useVirtualChessboard')[0]) {
$('#useVirtualChessboard')[0].checked = myVars.useVirtualChessboard;
// Show/hide the virtual chessboard container based on the setting
const virtualChessboardContainer = document.getElementById('virtualChessboardContainer');
if (virtualChessboardContainer) {
virtualChessboardContainer.style.display = myVars.useVirtualChessboard ? 'block' : 'none';
}
// If enabled, update the virtual chessboard with the current position
if (myVars.useVirtualChessboard) {
setTimeout(() => {
myFunctions.updateVirtualChessboard();
}, 500); // Slight delay to ensure the board is ready
}
}
// Set external window toggle
if ($('#useExternalWindow')[0]) {
$('#useExternalWindow')[0].checked = myVars.useExternalWindow;
// Show/hide external window options
if (myVars.useExternalWindow) {
$('#externalWindowOptions').show();
} else {
$('#externalWindowOptions').hide();
}
}
if (settings.timeDelayMin !== undefined && $('#timeDelayMin')[0]) {
$('#timeDelayMin')[0].value = settings.timeDelayMin;
}
if (settings.timeDelayMax !== undefined && $('#timeDelayMax')[0]) {
$('#timeDelayMax')[0].value = settings.timeDelayMax;
}
if (settings.evalBarTheme && $('#evalBarColor')[0]) {
$('#evalBarColor').val(settings.evalBarTheme);
if (settings.evalBarTheme === 'custom') {
$('#customColorContainer').show();
}
}
} else {
// Fallback to old method for backward compatibility
const savedDepth = await GM.getValue('depth', 11);
const savedElo = await GM.getValue('elo', 1500);
const savedAutoMove = await GM.getValue('autoMove', false);
const savedAutoRun = await GM.getValue('autoRun', false);
const savedAutoQueue = await GM.getValue('autoQueue', false);
const savedShowArrows = await GM.getValue('showArrows', true);
const savedPersistentHighlights = await GM.getValue('persistentHighlights', true);
const savedMoveIndicatorType = await GM.getValue('moveIndicatorType', 'highlights');
const savedHumanMode = await GM.getValue('humanMode', false);
const savedHumanLevel = await GM.getValue('humanLevel', 'intermediate');
const savedFusionMode = await GM.getValue('fusionMode', false);
const savedWhiteAdvantageColor = await GM.getValue('whiteAdvantageColor', '#4CAF50');
const savedBlackAdvantageColor = await GM.getValue('blackAdvantageColor', '#F44336');
// Apply saved settings
myVars.depth = savedDepth;
myVars.eloRating = savedElo;
myVars.autoMove = savedAutoMove;
myVars.autoRun = savedAutoRun;
myVars.autoQueue = savedAutoQueue;
myVars.showArrows = savedShowArrows;
myVars.persistentHighlights = savedPersistentHighlights;
myVars.moveIndicatorType = savedMoveIndicatorType;
myVars.humanMode = { active: savedHumanMode, level: savedHumanLevel };
myVars.fusionMode = savedFusionMode;
myVars.whiteAdvantageColor = savedWhiteAdvantageColor;
myVars.blackAdvantageColor = savedBlackAdvantageColor;
// Update UI elements to match saved settings
if ($('#depthSlider')[0]) {
$('#depthSlider')[0].value = savedDepth;
$('#depthText').html('Current Depth: <strong>' + savedDepth + '</strong>');
}
if ($('#eloSlider')[0]) {
$('#eloSlider')[0].value = savedElo;
$('#eloText').html('Current ELO: <strong>' + savedElo + '</strong>');
}
if ($('#autoMove')[0]) {
$('#autoMove')[0].checked = savedAutoMove;
}
if ($('#autoRun')[0]) {
$('#autoRun')[0].checked = savedAutoRun;
}
if ($('#autoQueue')[0]) {
$('#autoQueue')[0].checked = savedAutoQueue;
$('#autoQueueStatus').text(savedAutoQueue ? 'On' : 'Off');
$('#autoQueueStatus').css('color', savedAutoQueue ? '#9C27B0' : '#666');
}
if ($('#showArrows')[0]) {
$('#showArrows')[0].checked = savedShowArrows;
}
if ($('#persistentHighlights')[0]) {
$('#persistentHighlights')[0].checked = savedPersistentHighlights;
}
if ($('input[name="moveIndicatorType"]').length) {
$('input[name="moveIndicatorType"][value="' + savedMoveIndicatorType + '"]').prop('checked', true);
// Show/hide arrow customization container based on move indicator type
if (savedMoveIndicatorType === 'arrows') {
$('#arrowCustomizationContainer').show();
} else {
$('#arrowCustomizationContainer').hide();
}
}
if ($('#humanMode')[0]) {
$('#humanMode')[0].checked = savedHumanMode;
}
if ($('#humanLevelSelect')[0]) {
$('#humanLevelSelect')[0].value = savedHumanLevel;
}
if ($('#fusionMode')[0]) {
$('#fusionMode')[0].checked = savedFusionMode;
}
if ($('#whiteAdvantageColor')[0]) {
$('#whiteAdvantageColor')[0].value = savedWhiteAdvantageColor;
}
if ($('#blackAdvantageColor')[0]) {
$('#blackAdvantageColor')[0].value = savedBlackAdvantageColor;
}
// After loading the settings from individual values, save them as a combined object
// This will migrate users to the new format
myFunctions.saveSettings();
}
// Check for first run (always use individual setting for this)
const savedFirstRun = await GM.getValue('firstRun', true);
// Show welcome modal for first-time users
if (savedFirstRun) {
setTimeout(() => {
myFunctions.showWelcomeModal();
GM.setValue('firstRun', false);
}, 1000);
}
console.log('Settings loaded successfully');
// Initialize auto queue observer if enabled
myFunctions.updateAutoQueueObserver();
} catch (error) {
console.error('Error loading settings:', error);
}
}
// Function to show welcome modal for first-time users
function showWelcomeModal() {
// Create welcome modal
const welcomeModal = document.createElement('div');
welcomeModal.id = 'welcomeModal';
welcomeModal.style = `
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 2000;
justify-content: center;
align-items: center;
`;
const modalContent = document.createElement('div');
modalContent.style = `
background-color: white;
padding: 30px;
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
`;
const closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style = `
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #333;
transition: color 0.2s;
`;
closeBtn.onmouseover = function() {
this.style.color = '#F44336';
};
closeBtn.onmouseout = function() {
this.style.color = '#333';
};
closeBtn.onclick = function() {
welcomeModal.style.display = 'none';
};
modalContent.appendChild(closeBtn);
// Welcome content
const welcomeTitle = document.createElement('h2');
welcomeTitle.textContent = 'Welcome to Chess AI!';
welcomeTitle.style = 'margin-top: 0; color: #2196F3; border-bottom: 2px solid #eee; padding-bottom: 10px;';
modalContent.appendChild(welcomeTitle);
const welcomeText = document.createElement('p');
welcomeText.textContent = 'Thank you for installing Chess AI. This tool helps you analyze chess positions and find the best moves during your games on Chess.com.';
welcomeText.style = 'margin-bottom: 20px; color: #666;';
modalContent.appendChild(welcomeText);
// Quick start guide
const quickStartTitle = document.createElement('h3');
quickStartTitle.textContent = 'Quick Start Guide';
quickStartTitle.style = 'color: #4CAF50; margin-bottom: 15px;';
modalContent.appendChild(quickStartTitle);
const steps = [
{ title: 'Run the Engine', content: 'Press any key from Q to M to run the engine at different depths. Higher depths give stronger analysis but take longer.' },
{ title: 'View Best Moves', content: 'The best moves will be highlighted on the board, and the evaluation bar will show who has the advantage.' },
{ title: 'Adjust Settings', content: 'Click the settings icon to customize the engine strength, visual indicators, and auto-play options.' },
{ title: 'Keyboard Shortcuts', content: 'Use keyboard shortcuts for quick access. Press the "Keyboard Shortcuts" button to see all available shortcuts.' }
];
const stepsList = document.createElement('div');
stepsList.style = 'margin-bottom: 25px;';
steps.forEach((step, index) => {
const stepItem = document.createElement('div');
stepItem.style = 'margin-bottom: 15px; display: flex;';
const stepNumber = document.createElement('div');
stepNumber.textContent = (index + 1);
stepNumber.style = `
width: 25px;
height: 25px;
background-color: #2196F3;
color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
flex-shrink: 0;
font-weight: bold;
`;
const stepContent = document.createElement('div');
const stepTitle = document.createElement('div');
stepTitle.textContent = step.title;
stepTitle.style = 'font-weight: bold; margin-bottom: 5px;';
const stepDescription = document.createElement('div');
stepDescription.textContent = step.content;
stepDescription.style = 'color: #666;';
stepContent.appendChild(stepTitle);
stepContent.appendChild(stepDescription);
stepItem.appendChild(stepNumber);
stepItem.appendChild(stepContent);
stepsList.appendChild(stepItem);
});
modalContent.appendChild(stepsList);
// Tips section
const tipsTitle = document.createElement('h3');
tipsTitle.textContent = 'Pro Tips';
tipsTitle.style = 'color: #FF9800; margin-bottom: 15px;';
modalContent.appendChild(tipsTitle);
const tipsList = document.createElement('ul');
tipsList.style = 'margin-bottom: 25px; padding-left: 20px;';
const tips = [
'Use depths 1-10 for quick analysis and casual play.',
'Use depths 15+ for serious analysis and difficult positions.',
'Enable "Auto Move" to automatically play the best move.',
'Try "Human Mode" to get more natural, human-like suggestions.',
'Customize the evaluation bar colors in the Visual tab.'
];
tips.forEach(tip => {
const tipItem = document.createElement('li');
tipItem.textContent = tip;
tipItem.style = 'margin-bottom: 8px; color: #666;';
tipsList.appendChild(tipItem);
});
modalContent.appendChild(tipsList);
// Get started button
const getStartedBtn = document.createElement('button');
getStartedBtn.textContent = 'Get Started';
getStartedBtn.style = `
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 16px;
transition: background-color 0.2s;
`;
getStartedBtn.onmouseover = function() {
this.style.backgroundColor = '#45a049';
};
getStartedBtn.onmouseout = function() {
this.style.backgroundColor = '#4CAF50';
};
getStartedBtn.onclick = function() {
welcomeModal.style.display = 'none';
};
modalContent.appendChild(getStartedBtn);
welcomeModal.appendChild(modalContent);
document.body.appendChild(welcomeModal);
}
// The move history display is now embedded directly in the Actions tab HTML
// Add a move to the history
myFunctions.addMoveToHistory = function(move, evaluation, depth) {
const tableBody = document.getElementById('moveHistoryTableBody');
if (!tableBody) return;
const row = document.createElement('tr');
// Format the evaluation
let evalText = '';
if (typeof evaluation === 'string' && evaluation.includes('Mate')) {
evalText = evaluation;
} else {
const sign = evaluation > 0 ? '+' : '';
evalText = `${sign}${parseFloat(evaluation).toFixed(2)}`;
}
row.innerHTML = `
<td style="padding: 5px; border-bottom: 1px solid #ddd;">${move}</td>
<td style="padding: 5px; border-bottom: 1px solid #ddd;">${evalText}</td>
<td style="padding: 5px; border-bottom: 1px solid #ddd;">${depth}</td>
`;
// Add the new row at the top
if (tableBody.firstChild) {
tableBody.insertBefore(row, tableBody.firstChild);
} else {
tableBody.appendChild(row);
}
// Limit the number of rows to 50
while (tableBody.children.length > 50) {
tableBody.removeChild(tableBody.lastChild);
}
};
// Function to update the auto run status indicator
myFunctions.updateAutoRunStatus = function(status) {
if (!$('#autoRunStatus')[0]) return;
switch(status) {
case 'on':
$('#autoRunStatus').text('On');
$('#autoRunStatus').css('color', '#4CAF50');
break;
case 'off':
$('#autoRunStatus').text('Off');
$('#autoRunStatus').css('color', '#666');
break;
case 'waiting':
$('#autoRunStatus').text('Waiting...');
$('#autoRunStatus').css('color', '#FFA500');
break;
case 'running':
$('#autoRunStatus').text('Running...');
$('#autoRunStatus').css('color', '#2196F3');
break;
}
};
// Function to evaluate the complexity of the board position
function evaluateBoardComplexity(boardState) {
let complexity = 0;
// Example evaluation criteria
const pieceValues = {
'p': 1, // Pawn
'r': 5, // Rook
'n': 3, // Knight
'b': 3, // Bishop
'q': 9, // Queen
'k': 0, // King (not counted)
};
// Loop through the board state to evaluate material balance
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = boardState[row][col];
if (piece) {
const value = pieceValues[piece.toLowerCase()] || 0;
complexity += piece === piece.toUpperCase() ? value : -value; // Add for white, subtract for black
}
}
}
// Additional complexity factors can be added here
// For example, consider piece activity, control of the center, etc.
// This is a simple example; you can expand it based on your needs
// Normalize complexity to a reasonable range
complexity = Math.abs(complexity); // Ensure it's positive
return Math.floor(complexity / 10); // Scale down for thinking time calculation
}
myFunctions.extractOpponentRating = function() {
// Try to find the opponent's rating using the new selector
try {
// Try the new selector first
const ratingElement = document.querySelector("#board-layout-player-top .cc-user-rating-white");
if (ratingElement) {
const ratingText = ratingElement.textContent.trim();
const ratingMatch = ratingText.match(/\((\d+)\)/);
if (ratingMatch && ratingMatch[1]) {
const rating = parseInt(ratingMatch[1]);
if (!isNaN(rating)) {
console.log(`Opponent rating detected: ${rating}`);
return rating;
}
}
}
// Fallback to old selector if new one fails
const ratingElements = document.querySelectorAll('.user-tagline-rating');
if (ratingElements.length >= 2) {
// Find the element that doesn't match the player's username
const playerUsername = document.querySelector('.user-username-component')?.textContent.trim();
for (const element of ratingElements) {
const usernameElement = element.closest('.user-tagline')?.querySelector('.user-username-component');
if (usernameElement && usernameElement.textContent.trim() !== playerUsername) {
const rating = parseInt(element.textContent.trim());
if (!isNaN(rating)) {
console.log(`Opponent rating detected (fallback): ${rating}`);
return rating;
}
}
}
}
} catch (error) {
console.error('Error extracting opponent rating:', error);
}
return null;
}
// Function to show welcome modal for first-time users
myFunctions.showWelcomeModal = function() {
// Create welcome modal
const welcomeModal = document.createElement('div');
welcomeModal.id = 'welcomeModal';
welcomeModal.style = `
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
z-index: 2000;
justify-content: center;
align-items: center;
`;
const modalContent = document.createElement('div');
modalContent.style = `
background-color: white;
padding: 30px;
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
position: relative;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
`;
const closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style = `
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #333;
transition: color 0.2s;
`;
closeBtn.onmouseover = function() {
this.style.color = '#F44336';
};
closeBtn.onmouseout = function() {
this.style.color = '#333';
};
closeBtn.onclick = function() {
welcomeModal.style.display = 'none';
};
modalContent.appendChild(closeBtn);
// Welcome content
const welcomeTitle = document.createElement('h2');
welcomeTitle.textContent = 'Welcome to Chess AI!';
welcomeTitle.style = 'margin-top: 0; color: #2196F3; border-bottom: 2px solid #eee; padding-bottom: 10px;';
modalContent.appendChild(welcomeTitle);
const welcomeText = document.createElement('p');
welcomeText.textContent = 'Thank you for installing Chess AI. This tool helps you analyze chess positions and find the best moves during your games on Chess.com.';
welcomeText.style = 'margin-bottom: 20px; color: #666;';
modalContent.appendChild(welcomeText);
// Quick start guide
const quickStartTitle = document.createElement('h3');
quickStartTitle.textContent = 'Quick Start Guide';
quickStartTitle.style = 'color: #4CAF50; margin-bottom: 15px;';
modalContent.appendChild(quickStartTitle);
const steps = [
{ title: 'Run the Engine', content: 'Press any key from Q to M to run the engine at different depths. Higher depths give stronger analysis but take longer.' },
{ title: 'View Best Moves', content: 'The best moves will be highlighted on the board, and the evaluation bar will show who has the advantage.' },
{ title: 'Adjust Settings', content: 'Click the settings icon to customize the engine strength, visual indicators, and auto-play options.' },
{ title: 'Keyboard Shortcuts', content: 'Use keyboard shortcuts for quick access. Press the "Keyboard Shortcuts" button to see all available shortcuts.' }
];
const stepsList = document.createElement('div');
stepsList.style = 'margin-bottom: 25px;';
steps.forEach((step, index) => {
const stepItem = document.createElement('div');
stepItem.style = 'margin-bottom: 15px; display: flex;';
const stepNumber = document.createElement('div');
stepNumber.textContent = (index + 1);
stepNumber.style = `
width: 25px;
height: 25px;
background-color: #2196F3;
color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
flex-shrink: 0;
font-weight: bold;
`;
const stepContent = document.createElement('div');
const stepTitle = document.createElement('div');
stepTitle.textContent = step.title;
stepTitle.style = 'font-weight: bold; margin-bottom: 5px;';
const stepDescription = document.createElement('div');
stepDescription.textContent = step.content;
stepDescription.style = 'color: #666;';
stepContent.appendChild(stepTitle);
stepContent.appendChild(stepDescription);
stepItem.appendChild(stepNumber);
stepItem.appendChild(stepContent);
stepsList.appendChild(stepItem);
});
modalContent.appendChild(stepsList);
// Tips section
const tipsTitle = document.createElement('h3');
tipsTitle.textContent = 'Pro Tips';
tipsTitle.style = 'color: #FF9800; margin-bottom: 15px;';
modalContent.appendChild(tipsTitle);
const tipsList = document.createElement('ul');
tipsList.style = 'margin-bottom: 25px; padding-left: 20px;';
const tips = [
'Use depths 1-10 for quick analysis and casual play.',
'Use depths 15+ for serious analysis and difficult positions.',
'Enable "Auto Move" to automatically play the best move.',
'Try "Human Mode" to get more natural, human-like suggestions.',
'Customize the evaluation bar colors in the Visual tab.'
];
tips.forEach(tip => {
const tipItem = document.createElement('li');
tipItem.textContent = tip;
tipItem.style = 'margin-bottom: 8px; color: #666;';
tipsList.appendChild(tipItem);
});
modalContent.appendChild(tipsList);
// Get started button
const getStartedBtn = document.createElement('button');
getStartedBtn.textContent = 'Get Started';
getStartedBtn.style = `
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 16px;
transition: background-color 0.2s;
`;
getStartedBtn.onmouseover = function() {
this.style.backgroundColor = '#45a049';
};
getStartedBtn.onmouseout = function() {
this.style.backgroundColor = '#4CAF50';
};
getStartedBtn.onclick = function() {
welcomeModal.style.display = 'none';
};
modalContent.appendChild(getStartedBtn);
welcomeModal.appendChild(modalContent);
document.body.appendChild(welcomeModal);
}
}
//Touching below may break the script
var isThinking = false
var canGo = true;
var myTurn = false;
var board;
window.addEventListener("load", (event) => {
// Start the main application
main();
});